diff --git a/404.html b/404.html index 287df255ab..6de75da90a 100644 --- a/404.html +++ b/404.html @@ -16,8 +16,8 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

diff --git a/assets/images/banner-3f2086c61f3c010fc38db9701cd3d398.png b/assets/images/banner-3f2086c61f3c010fc38db9701cd3d398.png new file mode 100644 index 0000000000..c3aca222e6 Binary files /dev/null and b/assets/images/banner-3f2086c61f3c010fc38db9701cd3d398.png differ diff --git a/assets/js/09444585.3e9a300e.js b/assets/js/09444585.3e9a300e.js new file mode 100644 index 0000000000..5386bbc8cb --- /dev/null +++ b/assets/js/09444585.3e9a300e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2658],{43966:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>u,frontMatter:()=>l,metadata:()=>c,toc:()=>p});var t=r(74848),o=r(28453),s=r(11470),i=r(19365);const l={title:"OpenAPI & Swagger UI",sidebar_label:"OpenAPI"},a=void 0,c={id:"common/openapi-and-swagger-ui",title:"OpenAPI & Swagger UI",description:"Introduction",source:"@site/docs/common/openapi-and-swagger-ui.md",sourceDirName:"common",slug:"/common/openapi-and-swagger-ui",permalink:"/docs/common/openapi-and-swagger-ui",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/openapi-and-swagger-ui.md",tags:[],version:"current",frontMatter:{title:"OpenAPI & Swagger UI",sidebar_label:"OpenAPI"},sidebar:"someSidebar",previous:{title:"REST API",permalink:"/docs/common/rest-blueprints"},next:{title:"GraphQL",permalink:"/docs/common/graphql"}},d={},p=[{value:"Introduction",id:"introduction",level:2},{value:"Quick Start",id:"quick-start",level:2},{value:"OpenAPI",id:"openapi",level:2},{value:"The Basics",id:"the-basics",level:3},{value:"Don't Repeat Yourself and Decorate Sub-Controllers",id:"dont-repeat-yourself-and-decorate-sub-controllers",level:3},{value:"Use Existing Hooks",id:"use-existing-hooks",level:3},{value:"Generate the API Document",id:"generate-the-api-document",level:3},{value:"from the controllers",id:"from-the-controllers",level:4},{value:"from a shell script",id:"from-a-shell-script",level:4},{value:"Using the Swagger UI controller",id:"using-the-swagger-ui-controller",level:4},{value:"Swagger UI",id:"swagger-ui",level:2},{value:"Simple case",id:"simple-case",level:3},{value:"With an URL",id:"with-an-url",level:3},{value:"Several APIs or Versions",id:"several-apis-or-versions",level:3},{value:"Using a Static File",id:"using-a-static-file",level:3},{value:"Advanced",id:"advanced",level:2},{value:"Using Controller Properties",id:"using-controller-properties",level:3},{value:"In-Depth Overview",id:"in-depth-overview",level:3},{value:"Define and Reuse Components",id:"define-and-reuse-components",level:3},{value:"Generate and Save a Specification File with a Shell Script",id:"generate-and-save-a-specification-file-with-a-shell-script",level:3},{value:"Common Errors",id:"common-errors",level:3},{value:"Extend Swagger UI options",id:"extend-swagger-ui-options",level:3}];function h(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"OpenAPI Specification"})," (formerly known as Swagger Specification) is an API description format for REST APIs. An OpenAPI ",(0,t.jsx)(n.em,{children:"document"})," allows developers to describe entirely an API."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Swagger UI"})," is a graphical interface to visualize and interact with the API\u2019s resources. It is automatically generated from one or several OpenAPI documents."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:(0,t.jsx)(n.a,{href:"https://editor.swagger.io/",children:"Example of OpenAPI document and Swagger Visualisation"})})}),"\n",(0,t.jsx)(n.h2,{id:"quick-start",children:"Quick Start"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This example shows how to generate a documentation page of your API directly from your hooks and controllers."})}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller } from '@foal/core';\n\nimport { ApiController, OpenApiController } from './controllers';\n\nexport class AppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenApiController),\n ]\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiInfo, ApiServer, Context, Post, ValidateBody } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n@ApiServer({\n url: '/api'\n})\n@JWTRequired()\nexport class ApiController {\n\n @Post('/products')\n @ValidateBody({\n type: 'object',\n properties: {\n name: { type: 'string' }\n },\n required: [ 'name' ],\n additionalProperties: false,\n })\n createProduct(ctx: Context) {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"openapi.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiController } from './api.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = { controllerClass: ApiController };\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Result"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Swagger page",src:r(84886).A+"",width:"2560",height:"1600"})}),"\n",(0,t.jsx)(n.h2,{id:"openapi",children:"OpenAPI"}),"\n",(0,t.jsx)(n.h3,{id:"the-basics",children:"The Basics"}),"\n",(0,t.jsxs)(n.p,{children:["The first thing to do is to add the ",(0,t.jsx)(n.code,{children:"@ApiInfo"})," decorator to the root controller of the API. Two attributes are required: the ",(0,t.jsx)(n.code,{children:"title"})," and the ",(0,t.jsx)(n.code,{children:"version"})," of the API."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiInfo } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n// @ApiServer({\n// url: '/api'\n// })\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Then each controller method can be documented with the ",(0,t.jsx)(n.code,{children:"@ApiOperation"})," decorator."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiOperation, Get } from '@foal/core';\n\n// ...\nexport class ApiController {\n @Get('/products')\n @ApiOperation({\n responses: {\n 200: {\n content: {\n 'application/json': {\n schema: {\n items: {\n properties: {\n name: { type: 'string' }\n },\n type: 'object',\n required: [ 'name' ]\n },\n type: 'array',\n }\n }\n },\n description: 'successful operation'\n }\n },\n summary: 'Return a list of all the products.'\n })\n readProducts() {\n // ...\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Beside the ",(0,t.jsx)(n.code,{children:"@ApiOperation"})," decorator, you can also use other decorators more specific to improve the readability of the code."]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.th,{children:"Operation Decorators"})})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiOperationSummary"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiOperationId"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiOperationDescription"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiServer"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiRequestBody"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiSecurityRequirement"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineTag"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiExternalDoc"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiUseTag"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiParameter"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiResponse"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiCallback"})})})]})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiOperation, ApiResponse, Get } from '@foal/core';\n// ...\nexport class ApiController {\n\n @Get('/products')\n @ApiOperation({\n responses: {\n 200: {\n description: 'successful operation'\n },\n 404: {\n description: 'not found'\n },\n }\n })\n readProducts() {\n // ...\n }\n\n // is equivalent to\n\n @Get('/products')\n @ApiResponse(200, { description: 'successful operation' })\n @ApiResponse(404, { description: 'not found' })\n readProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"dont-repeat-yourself-and-decorate-sub-controllers",children:"Don't Repeat Yourself and Decorate Sub-Controllers"}),"\n",(0,t.jsx)(n.p,{children:"Large applications can have many subcontrollers. FoalTS automatically resolves the paths for you and allows you to share common specifications between several operations."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiDeprecated, ApiInfo, ApiResponse, controller, Get } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\nexport class ApiController {\n subControllers = [\n controller('/products', ProductController)\n ];\n}\n\n// All the operations of this controller and\n// its subcontrollers should be deprecated.\n@ApiDeprecated()\nclass ProductController {\n\n @Get()\n @ApiResponse(200, { description: 'successful operation' })\n readProducts() {\n // ...\n }\n\n @Get('/:productId')\n @ApiResponse(200, { description: 'successful operation' })\n @ApiResponse(404, { description: 'not found' })\n readProduct() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"The generated document will then look like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"openapi: 3.0.0\ninfo:\n title: 'A Great API'\n version: 1.0.0\npaths:\n /products: # The path is computed automatically\n get:\n deprecated: true # The operation is deprecated\n responses:\n 200:\n description: successful operation\n /products/{productId}: # The path is computed automatically\n get:\n deprecated: true # The operation is deprecated\n responses:\n 200:\n description: successful operation\n 404:\n description: not found\n"})}),"\n",(0,t.jsx)(n.h3,{id:"use-existing-hooks",children:"Use Existing Hooks"}),"\n",(0,t.jsx)(n.p,{children:"The addition of these decorators can be quite redundant with existing hooks. For example, if we want to write OpenAPI documentation for authentication and validation of the request body, we may end up with something like this."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@JWTRequired()\n@ApiSecurityRequirement({ bearerAuth: [] })\n@ApiDefineSecurityScheme('bearerAuth', {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT'\n})\nexport class ApiController {\n \n @Post('/products')\n @ValidateBody(schema)\n @ApiRequestBody({\n required: true,\n content: {\n 'application/json': { schema }\n }\n })\n createProducts() {\n \n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"To avoid this, the framework hooks already expose an API specification which is directly included in the generated OpenAPI document."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@JWTRequired()\nexport class ApiController {\n \n @Post('/products')\n @ValidateBody(schema)\n createProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can disable this behavior globally with the ",(0,t.jsx)(n.a,{href:"/docs/architecture/configuration",children:"configuration key"})," ",(0,t.jsx)(n.code,{children:"setting.openapi.useHooks"}),"."]}),"\n",(0,t.jsxs)(s.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n openapi:\n useHooks: false\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "openapi": {\n "useHooks": false\n }\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-js",children:"module.exports = {\n settings: {\n openapi: {\n useHooks: false\n }\n }\n}\n"})})})]}),"\n",(0,t.jsxs)(n.p,{children:["You can also disable it on a specific hook with the ",(0,t.jsx)(n.code,{children:"openapi"})," option."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class ApiController {\n \n @Post('/products')\n // Generate automatically the OpenAPI spec for the request body\n @ValidateBody(schema)\n // Choose to write a customize spec for the path parameters\n @ValidateParams(schema2, { openapi: false })\n @ApiParameter( ... )\n createProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"generate-the-api-document",children:"Generate the API Document"}),"\n",(0,t.jsx)(n.p,{children:"Once the controllers are decorated, there are several ways to generate the OpenAPI document."}),"\n",(0,t.jsx)(n.h4,{id:"from-the-controllers",children:"from the controllers"}),"\n",(0,t.jsxs)(n.p,{children:["Documents can be retrieved with the ",(0,t.jsx)(n.code,{children:"OpenApi"})," service:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, OpenApi } from '@foal/core';\n\nclass Service {\n @dependency\n openApi: OpenApi;\n\n foo() {\n const document = this.openApi.getDocument(ApiController);\n }\n}\n\n"})}),"\n",(0,t.jsx)(n.h4,{id:"from-a-shell-script",children:"from a shell script"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npx foal generate script generate-openapi-doc\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"createOpenApiDocument"})," function can also be used in a shell script to generate the document. You can provide it with an optional serviceManager if needed."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note that this function instantiates the controllers. So if you have logic in your constructors, you may prefer to put it in ",(0,t.jsx)(n.code,{children:"init"})," methods."]})}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/scripts/generate-openapi-doc.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { writeFileSync } from 'fs';\n\n// 3p\nimport { createOpenApiDocument } from '@foal/core';\nimport { stringify } from 'yamljs';\n\n// App\nimport { ApiController } from '../app/controllers';\n\nexport async function main() {\n const document = createOpenApiDocument(ApiController);\n const yamlDocument = stringify(document);\n\n writeFileSync('openapi.yml', yamlDocument, 'utf8');\n}\n\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run build\nnpx foal run generate-openapi-doc\n"})}),"\n",(0,t.jsx)(n.h4,{id:"using-the-swagger-ui-controller",children:"Using the Swagger UI controller"}),"\n",(0,t.jsxs)(n.p,{children:["Another alternative is to use the ",(0,t.jsx)(n.a,{href:"#Swagger%20UI",children:"SwaggerController"})," directly. This allows you to serve the document(s) at ",(0,t.jsx)(n.code,{children:"/openapi.json"})," and to use it (them) in a Swagger interface."]}),"\n",(0,t.jsx)(n.h2,{id:"swagger-ui",children:"Swagger UI"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Example of Swagger UI",src:r(18559).A+"",width:"964",height:"1162"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install @foal/swagger\n"})}),"\n",(0,t.jsx)(n.h3,{id:"simple-case",children:"Simple case"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiController, OpenApiController } from './controllers';\n\nexport class AppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenApiController)\n ]\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"open-api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiController } from './api.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = { controllerClass: ApiController };\n}\n\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Opening the browser at the path ",(0,t.jsx)(n.code,{children:"/swagger"})," will display the documentation of the ",(0,t.jsx)(n.code,{children:"ApiController"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"with-an-url",children:"With an URL"}),"\n",(0,t.jsx)(n.p,{children:"If needed, you can also specify the URL of a custom OpenAPI file (YAML or JSON)."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nexport class OpenApiController extends SwaggerController {\n options = { url: 'https://petstore.swagger.io/v2/swagger.json' };\n}\n\n"})}),"\n",(0,t.jsx)(n.h3,{id:"several-apis-or-versions",children:"Several APIs or Versions"}),"\n",(0,t.jsx)(n.p,{children:"Some applications may serve several APIs (for example two versions of a same API). Here is an example on how to handle this."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Example of several versions",src:r(79252).A+"",width:"1680",height:"504"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller } from '@foal/core';\n\nimport { ApiV1Controller, ApiV2ontroller, OpenApiController } from './controllers';\n\nexport class AppController {\n subControllers = [\n controller('/api', ApiV1Controller),\n controller('/api2', ApiV2Controller),\n controller('/swagger', OpenApiController),\n ]\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"open-api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiV1Controller } from './api-v1.controller';\nimport { ApiV2Controller } from './api-v2.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = [\n { name: 'v1', controllerClass: ApiV1Controller },\n { name: 'v2', controllerClass: ApiV2Controller, primary: true },\n ]\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"using-a-static-file",children:"Using a Static File"}),"\n",(0,t.jsxs)(n.p,{children:["If you prefer to write manually your OpenAPI document, you can add an ",(0,t.jsx)(n.code,{children:"openapi.yml"})," file in the ",(0,t.jsx)(n.code,{children:"public/"})," directory and configure your ",(0,t.jsx)(n.code,{children:"SwaggerController"})," as follows:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nexport class OpenApiController extends SwaggerController {\n options = { url: '/openapi.yml' };\n}\n\n"})}),"\n",(0,t.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,t.jsx)(n.h3,{id:"using-controller-properties",children:"Using Controller Properties"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiRequestBody, IApiRequestBody, Post } from '@foal/core';\n\nclass ApiController {\n\n requestBody: IApiRequestBody = {\n content: {\n 'application/json': {\n schema: {\n type: 'object'\n }\n }\n },\n required: true\n };\n\n @Post('/products')\n // This is invalid:\n // @ApiRequestBody(this.requestBody)\n // This is valid:\n @ApiRequestBody(controller => controller.requestBody)\n createProduct() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"in-depth-overview",children:"In-Depth Overview"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"FoalTS automatically resolves the path items and operations based on your controller paths."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiResponse, Get, Post } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\nexport class ApiController {\n\n @Get('/products')\n @ApiResponse(200, { description: 'successful operation' })\n readProducts() {\n // ...\n }\n\n @Post('/products')\n @ApiResponse(200, { description: 'successful operation' })\n createProduct() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:'openapi: 3.0.0\ninfo:\n title: \'A Great API\'\n version: 1.0.0\npaths:\n /products: # Foal automatically puts the "get" and "post" operations under the same path item as required by OpenAPI rules.\n get:\n responses:\n 200:\n description: successful operation\n post:\n responses:\n 200:\n description: successful operation\n'})}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["The decorators ",(0,t.jsx)(n.code,{children:"@ApiServer"}),", ",(0,t.jsx)(n.code,{children:"@ApiSecurityRequirement"})," and ",(0,t.jsx)(n.code,{children:"@ApiExternalDocs"})," have a different behavior depending on if they decorate the root controller or a subcontroller / a method."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with the root controller"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiResponse, ApiServer } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n@ApiServer({ url: 'http://example.com' })\nexport class ApiController {\n\n // ...\n\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"openapi: 3.0.0\ninfo:\n title: 'A Great API'\n version: 1.0.0\npaths:\n # ...\nservers:\n- url: http://example.com\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a subcontroller / a method"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiResponse, ApiServer, Get } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\nexport class ApiController {\n\n @Get('/')\n @ApiServer({ url: 'http://example.com' })\n @ApiResponse(200, { description: 'successful operation' })\n index() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"openapi: 3.0.0\ninfo:\n title: 'A Great API'\n version: 1.0.0\npaths:\n /:\n get:\n responses:\n 200:\n description: successful operation\n servers:\n - url: http://example.com\n"})}),"\n",(0,t.jsx)(n.h3,{id:"define-and-reuse-components",children:"Define and Reuse Components"}),"\n",(0,t.jsx)(n.p,{children:"OpenAPI allows you to define and reuse components. Here is a way to achieve this with Foal."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiInfo, ApiDefineSchema, Get } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n@ApiDefineSchema('product', {\n type: 'object',\n properties: {\n name: { type: 'string' }\n }\n required: [ 'name' ]\n})\nexport class ApiController {\n\n @Get('/products/:productId')\n @ApiResponse(200, {\n description: 'successful operation'\n content: {\n 'application/json': {\n schema: { $ref: '#/components/schemas/product' }\n }\n }\n })\n readProducts() {\n // ...\n }\n\n @Get('/products')\n @ApiResponse(200, {\n description: 'successful operation',\n content: {\n 'application/json': {\n schema: {\n type: 'array',\n items: { $ref: '#/components/schemas/product' }\n }\n }\n }\n })\n readProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.th,{children:"Component Decorators"})})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineSchema"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineResponse"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineParameter"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineExample"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineRequestBody"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineHeader"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineSecurityScheme"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineLink"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineCallback"})})})]})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"@ApiDefineXXX"})," decorators can be added to any controllers or methods but they always define components in the global scope of the API the controller belongs to."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The schemas defined with these decorators can also be re-used in the ",(0,t.jsx)(n.code,{children:"@ValidateXXX"})," hooks."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const productSchema = {\n // ...\n}\n\n@ApiDefineSchema('product', productSchema)\n@ValidateBody({\n $ref: '#/components/schemas/product'\n})\n"})}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"generate-and-save-a-specification-file-with-a-shell-script",children:"Generate and Save a Specification File with a Shell Script"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npx foal generate script generate-openapi-doc\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/scripts/generate-openapi-doc.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { writeFileSync } from 'fs';\n\n// 3p\nimport { createOpenApiDocument } from '@foal/core';\nimport { stringify } from 'yamljs';\n\n// App\nimport { ApiController } from '../app/controllers';\n\nexport async function main() {\n const document = createOpenApiDocument(ApiController);\n const yamlDocument = stringify(document);\n\n writeFileSync('openapi.yml', yamlDocument, 'utf8');\n}\n\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run build\nnpx foal run generate-openapi-doc\n"})}),"\n",(0,t.jsx)(n.h3,{id:"common-errors",children:"Common Errors"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// ...\nexport class ApiController {\n @Get('/products/:id')\n getProduct() {\n return new HttpResponseOK();\n }\n\n @Put('/products/:productId')\n updateProduct() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"This example will throw this error."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Error: Templated paths with the same hierarchy but different templated names MUST NOT exist as they are identical.\n Path 1: /products/{id}\n Path 2: /products/{productId}\n"})}),"\n",(0,t.jsx)(n.p,{children:"OpenAPI does not support paths that are identical with different parameter names. Here is a way to solve this issue:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// ...\nexport class ApiController {\n @Get('/products/:productId')\n getProduct() {\n return new HttpResponseOK();\n }\n\n @Put('/products/:productId')\n updateProduct() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"extend-swagger-ui-options",children:"Extend Swagger UI options"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/",children:"Swagger UI options"})," can be extended using the ",(0,t.jsx)(n.code,{children:"uiOptions"})," property."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiController } from './api.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = { controllerClass: ApiController };\n\n uiOptions = { docExpansion: 'full' };\n}\n\n"})})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},19365:(e,n,r)=>{r.d(n,{A:()=>i});r(96540);var t=r(34164);const o={tabItem:"tabItem_Ymn6"};var s=r(74848);function i(e){let{children:n,hidden:r,className:i}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,t.A)(o.tabItem,i),hidden:r,children:n})}},11470:(e,n,r)=>{r.d(n,{A:()=>b});var t=r(96540),o=r(34164),s=r(23104),i=r(56347),l=r(205),a=r(57485),c=r(31682),d=r(89466);function p(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return p(e).map((e=>{let{props:{value:n,label:r,attributes:t,default:o}}=e;return{value:n,label:r,attributes:t,default:o}}))}(r);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,r])}function u(e){let{value:n,tabValues:r}=e;return r.some((e=>e.value===n))}function m(e){let{queryString:n=!1,groupId:r}=e;const o=(0,i.W6)(),s=function(e){let{queryString:n=!1,groupId:r}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:n,groupId:r});return[(0,a.aZ)(s),(0,t.useCallback)((e=>{if(!s)return;const n=new URLSearchParams(o.location.search);n.set(s,e),o.replace({...o.location,search:n.toString()})}),[s,o])]}function x(e){const{defaultValue:n,queryString:r=!1,groupId:o}=e,s=h(e),[i,a]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!u({value:n,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=r.find((e=>e.default))??r[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:s}))),[c,p]=m({queryString:r,groupId:o}),[x,j]=function(e){let{groupId:n}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(n),[o,s]=(0,d.Dv)(r);return[o,(0,t.useCallback)((e=>{r&&s.set(e)}),[r,s])]}({groupId:o}),g=(()=>{const e=c??x;return u({value:e,tabValues:s})?e:null})();(0,l.A)((()=>{g&&a(g)}),[g]);return{selectedValue:i,selectValue:(0,t.useCallback)((e=>{if(!u({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);a(e),p(e),j(e)}),[p,j,s]),tabValues:s}}var j=r(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var f=r(74848);function A(e){let{className:n,block:r,selectedValue:t,selectValue:i,tabValues:l}=e;const a=[],{blockElementScrollPositionUntilNextRender:c}=(0,s.a_)(),d=e=>{const n=e.currentTarget,r=a.indexOf(n),o=l[r].value;o!==t&&(c(n),i(o))},p=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=a.indexOf(e.currentTarget)+1;n=a[r]??a[0];break}case"ArrowLeft":{const r=a.indexOf(e.currentTarget)-1;n=a[r]??a[a.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":r},n),children:l.map((e=>{let{value:n,label:r,attributes:s}=e;return(0,f.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>a.push(e),onKeyDown:p,onClick:d,...s,className:(0,o.A)("tabs__item",g.tabItem,s?.className,{"tabs__item--active":t===n}),children:r??n},n)}))})}function v(e){let{lazy:n,children:r,selectedValue:o}=e;const s=(Array.isArray(r)?r:[r]).filter(Boolean);if(n){const e=s.find((e=>e.props.value===o));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:s.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==o})))})}function y(e){const n=x(e);return(0,f.jsxs)("div",{className:(0,o.A)("tabs-container",g.tabList),children:[(0,f.jsx)(A,{...e,...n}),(0,f.jsx)(v,{...e,...n})]})}function b(e){const n=(0,j.A)();return(0,f.jsx)(y,{...e,children:p(e.children)},String(n))}},84886:(e,n,r)=>{r.d(n,{A:()=>t});const t=r.p+"assets/images/openapi-quick-start-4ec0b3bb97350bd038b2015be28a1381.png"},18559:(e,n,r)=>{r.d(n,{A:()=>t});const t=r.p+"assets/images/swagger-a641f2dfd065149a6d8c2e98ccad465e.png"},79252:(e,n,r)=>{r.d(n,{A:()=>t});const t=r.p+"assets/images/swagger3-3d35a6bde1ec1156853e65e4be372e6e.png"},28453:(e,n,r)=>{r.d(n,{R:()=>i,x:()=>l});var t=r(96540);const o={},s=t.createContext(o);function i(e){const n=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/09444585.62ba9b73.js b/assets/js/09444585.62ba9b73.js deleted file mode 100644 index b352649be2..0000000000 --- a/assets/js/09444585.62ba9b73.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2658],{43966:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>u,frontMatter:()=>l,metadata:()=>c,toc:()=>p});var t=r(74848),o=r(28453),s=r(11470),i=r(19365);const l={title:"OpenAPI & Swagger UI",sidebar_label:"OpenAPI"},a=void 0,c={id:"common/openapi-and-swagger-ui",title:"OpenAPI & Swagger UI",description:"Introduction",source:"@site/docs/common/openapi-and-swagger-ui.md",sourceDirName:"common",slug:"/common/openapi-and-swagger-ui",permalink:"/docs/common/openapi-and-swagger-ui",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/openapi-and-swagger-ui.md",tags:[],version:"current",frontMatter:{title:"OpenAPI & Swagger UI",sidebar_label:"OpenAPI"},sidebar:"someSidebar",previous:{title:"REST API",permalink:"/docs/common/rest-blueprints"},next:{title:"GraphQL",permalink:"/docs/common/graphql"}},d={},p=[{value:"Introduction",id:"introduction",level:2},{value:"Quick Start",id:"quick-start",level:2},{value:"OpenAPI",id:"openapi",level:2},{value:"The Basics",id:"the-basics",level:3},{value:"Don't Repeat Yourself and Decorate Sub-Controllers",id:"dont-repeat-yourself-and-decorate-sub-controllers",level:3},{value:"Use Existing Hooks",id:"use-existing-hooks",level:3},{value:"Generate the API Document",id:"generate-the-api-document",level:3},{value:"from the controllers",id:"from-the-controllers",level:4},{value:"from a shell script",id:"from-a-shell-script",level:4},{value:"Using the Swagger UI controller",id:"using-the-swagger-ui-controller",level:4},{value:"Swagger UI",id:"swagger-ui",level:2},{value:"Simple case",id:"simple-case",level:3},{value:"With an URL",id:"with-an-url",level:3},{value:"Several APIs or Versions",id:"several-apis-or-versions",level:3},{value:"Using a Static File",id:"using-a-static-file",level:3},{value:"Advanced",id:"advanced",level:2},{value:"Using Controller Properties",id:"using-controller-properties",level:3},{value:"In-Depth Overview",id:"in-depth-overview",level:3},{value:"Define and Reuse Components",id:"define-and-reuse-components",level:3},{value:"Generate and Save a Specification File with a Shell Script",id:"generate-and-save-a-specification-file-with-a-shell-script",level:3},{value:"Common Errors",id:"common-errors",level:3},{value:"Extend Swagger UI options",id:"extend-swagger-ui-options",level:3}];function h(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"OpenAPI Specification"})," (formerly known as Swagger Specification) is an API description format for REST APIs. An OpenAPI ",(0,t.jsx)(n.em,{children:"document"})," allows developers to describe entirely an API."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Swagger UI"})," is a graphical interface to visualize and interact with the API\u2019s resources. It is automatically generated from one or several OpenAPI documents."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:(0,t.jsx)(n.a,{href:"https://editor.swagger.io/",children:"Example of OpenAPI document and Swagger Visualisation"})})}),"\n",(0,t.jsx)(n.h2,{id:"quick-start",children:"Quick Start"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This example shows how to generate a documentation page of your API directly from your hooks and controllers."})}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller } from '@foal/core';\n\nimport { ApiController, OpenApiController } from './controllers';\n\nexport class AppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenApiController),\n ]\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiInfo, ApiServer, Context, Post, ValidateBody } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n@ApiServer({\n url: '/api'\n})\n@JWTRequired()\nexport class ApiController {\n\n @Post('/products')\n @ValidateBody({\n type: 'object',\n properties: {\n name: { type: 'string' }\n },\n required: [ 'name' ],\n additionalProperties: false,\n })\n createProduct(ctx: Context) {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"openapi.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiController } from './api.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = { controllerClass: ApiController };\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Result"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Swagger page",src:r(84886).A+"",width:"2560",height:"1600"})}),"\n",(0,t.jsx)(n.h2,{id:"openapi",children:"OpenAPI"}),"\n",(0,t.jsx)(n.h3,{id:"the-basics",children:"The Basics"}),"\n",(0,t.jsxs)(n.p,{children:["The first thing to do is to add the ",(0,t.jsx)(n.code,{children:"@ApiInfo"})," decorator to the root controller of the API. Two attributes are required: the ",(0,t.jsx)(n.code,{children:"title"})," and the ",(0,t.jsx)(n.code,{children:"version"})," of the API."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiInfo } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n// @ApiServer({\n// url: '/api'\n// })\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Then each controller method can be documented with the ",(0,t.jsx)(n.code,{children:"@ApiOperation"})," decorator."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiOperation, Get } from '@foal/core';\n\n// ...\nexport class ApiController {\n @Get('/products')\n @ApiOperation({\n responses: {\n 200: {\n content: {\n 'application/json': {\n schema: {\n items: {\n properties: {\n name: { type: 'string' }\n },\n type: 'object',\n required: [ 'name' ]\n },\n type: 'array',\n }\n }\n },\n description: 'successful operation'\n }\n },\n summary: 'Return a list of all the products.'\n })\n readProducts() {\n // ...\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Beside the ",(0,t.jsx)(n.code,{children:"@ApiOperation"})," decorator, you can also use other decorators more specific to improve the readability of the code."]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.th,{children:"Operation Decorators"})})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiOperationSummary"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiOperationId"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiOperationDescription"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiServer"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiRequestBody"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiSecurityRequirement"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineTag"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiExternalDoc"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiUseTag"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiParameter"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiResponse"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiCallback"})})})]})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiOperation, ApiResponse, Get } from '@foal/core';\n// ...\nexport class ApiController {\n\n @Get('/products')\n @ApiOperation({\n responses: {\n 200: {\n description: 'successful operation'\n },\n 404: {\n description: 'not found'\n },\n }\n })\n readProducts() {\n // ...\n }\n\n // is equivalent to\n\n @Get('/products')\n @ApiResponse(200, { description: 'successful operation' })\n @ApiResponse(404, { description: 'not found' })\n readProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"dont-repeat-yourself-and-decorate-sub-controllers",children:"Don't Repeat Yourself and Decorate Sub-Controllers"}),"\n",(0,t.jsx)(n.p,{children:"Large applications can have many subcontrollers. FoalTS automatically resolves the paths for you and allows you to share common specifications between several operations."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiDeprecated, ApiInfo, ApiResponse, controller, Get } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\nexport class ApiController {\n subControllers = [\n controller('/products', ProductController)\n ];\n}\n\n// All the operations of this controller and\n// its subcontrollers should be deprecated.\n@ApiDeprecated()\nclass ProductController {\n\n @Get()\n @ApiResponse(200, { description: 'successful operation' })\n readProducts() {\n // ...\n }\n\n @Get('/:productId')\n @ApiResponse(200, { description: 'successful operation' })\n @ApiResponse(404, { description: 'not found' })\n readProduct() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"The generated document will then look like this:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"openapi: 3.0.0\ninfo:\n title: 'A Great API'\n version: 1.0.0\npaths:\n /products: # The path is computed automatically\n get:\n deprecated: true # The operation is deprecated\n responses:\n 200:\n description: successful operation\n /products/{productId}: # The path is computed automatically\n get:\n deprecated: true # The operation is deprecated\n responses:\n 200:\n description: successful operation\n 404:\n description: not found\n"})}),"\n",(0,t.jsx)(n.h3,{id:"use-existing-hooks",children:"Use Existing Hooks"}),"\n",(0,t.jsx)(n.p,{children:"The addition of these decorators can be quite redundant with existing hooks. For example, if we want to write OpenAPI documentation for authentication and validation of the request body, we may end up with something like this."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@JWTRequired()\n@ApiSecurityRequirement({ bearerAuth: [] })\n@ApiDefineSecurityScheme('bearerAuth', {\n type: 'http',\n scheme: 'bearer',\n bearerFormat: 'JWT'\n})\nexport class ApiController {\n \n @Post('/products')\n @ValidateBody(schema)\n @ApiRequestBody({\n required: true,\n content: {\n 'application/json': { schema }\n }\n })\n createProducts() {\n \n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"To avoid this, the framework hooks already expose an API specification which is directly included in the generated OpenAPI document."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@JWTRequired()\nexport class ApiController {\n \n @Post('/products')\n @ValidateBody(schema)\n createProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can disable this behavior globally with the ",(0,t.jsx)(n.a,{href:"/docs/architecture/configuration",children:"configuration key"})," ",(0,t.jsx)(n.code,{children:"setting.openapi.useHooks"}),"."]}),"\n",(0,t.jsxs)(s.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n openapi:\n useHooks: false\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "openapi": {\n "useHooks": false\n }\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-js",children:"module.exports = {\n settings: {\n openapi: {\n useHooks: false\n }\n }\n}\n"})})})]}),"\n",(0,t.jsxs)(n.p,{children:["You can also disable it on a specific hook with the ",(0,t.jsx)(n.code,{children:"openapi"})," option."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class ApiController {\n \n @Post('/products')\n // Generate automatically the OpenAPI spec for the request body\n @ValidateBody(schema)\n // Choose to write a customize spec for the path parameters\n @ValidateParams(schema2, { openapi: false })\n @ApiParameter( ... )\n createProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"generate-the-api-document",children:"Generate the API Document"}),"\n",(0,t.jsx)(n.p,{children:"Once the controllers are decorated, there are several ways to generate the OpenAPI document."}),"\n",(0,t.jsx)(n.h4,{id:"from-the-controllers",children:"from the controllers"}),"\n",(0,t.jsxs)(n.p,{children:["Documents can be retrieved with the ",(0,t.jsx)(n.code,{children:"OpenApi"})," service:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, OpenApi } from '@foal/core';\n\nclass Service {\n @dependency\n openApi: OpenApi;\n\n foo() {\n const document = this.openApi.getDocument(ApiController);\n }\n}\n\n"})}),"\n",(0,t.jsx)(n.h4,{id:"from-a-shell-script",children:"from a shell script"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"foal generate script generate-openapi-doc\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"createOpenApiDocument"})," function can also be used in a shell script to generate the document. You can provide it with an optional serviceManager if needed."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note that this function instantiates the controllers. So if you have logic in your constructors, you may prefer to put it in ",(0,t.jsx)(n.code,{children:"init"})," methods."]})}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/scripts/generate-openapi-doc.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { writeFileSync } from 'fs';\n\n// 3p\nimport { createOpenApiDocument } from '@foal/core';\nimport { stringify } from 'yamljs';\n\n// App\nimport { ApiController } from '../app/controllers';\n\nexport async function main() {\n const document = createOpenApiDocument(ApiController);\n const yamlDocument = stringify(document);\n\n writeFileSync('openapi.yml', yamlDocument, 'utf8');\n}\n\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run build\nfoal run generate-openapi-doc\n"})}),"\n",(0,t.jsx)(n.h4,{id:"using-the-swagger-ui-controller",children:"Using the Swagger UI controller"}),"\n",(0,t.jsxs)(n.p,{children:["Another alternative is to use the ",(0,t.jsx)(n.a,{href:"#Swagger%20UI",children:"SwaggerController"})," directly. This allows you to serve the document(s) at ",(0,t.jsx)(n.code,{children:"/openapi.json"})," and to use it (them) in a Swagger interface."]}),"\n",(0,t.jsx)(n.h2,{id:"swagger-ui",children:"Swagger UI"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Example of Swagger UI",src:r(18559).A+"",width:"964",height:"1162"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install @foal/swagger\n"})}),"\n",(0,t.jsx)(n.h3,{id:"simple-case",children:"Simple case"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiController, OpenApiController } from './controllers';\n\nexport class AppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenApiController)\n ]\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"open-api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiController } from './api.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = { controllerClass: ApiController };\n}\n\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Opening the browser at the path ",(0,t.jsx)(n.code,{children:"/swagger"})," will display the documentation of the ",(0,t.jsx)(n.code,{children:"ApiController"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"with-an-url",children:"With an URL"}),"\n",(0,t.jsx)(n.p,{children:"If needed, you can also specify the URL of a custom OpenAPI file (YAML or JSON)."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nexport class OpenApiController extends SwaggerController {\n options = { url: 'https://petstore.swagger.io/v2/swagger.json' };\n}\n\n"})}),"\n",(0,t.jsx)(n.h3,{id:"several-apis-or-versions",children:"Several APIs or Versions"}),"\n",(0,t.jsx)(n.p,{children:"Some applications may serve several APIs (for example two versions of a same API). Here is an example on how to handle this."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Example of several versions",src:r(79252).A+"",width:"1680",height:"504"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller } from '@foal/core';\n\nimport { ApiV1Controller, ApiV2ontroller, OpenApiController } from './controllers';\n\nexport class AppController {\n subControllers = [\n controller('/api', ApiV1Controller),\n controller('/api2', ApiV2Controller),\n controller('/swagger', OpenApiController),\n ]\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"open-api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiV1Controller } from './api-v1.controller';\nimport { ApiV2Controller } from './api-v2.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = [\n { name: 'v1', controllerClass: ApiV1Controller },\n { name: 'v2', controllerClass: ApiV2Controller, primary: true },\n ]\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"using-a-static-file",children:"Using a Static File"}),"\n",(0,t.jsxs)(n.p,{children:["If you prefer to write manually your OpenAPI document, you can add an ",(0,t.jsx)(n.code,{children:"openapi.yml"})," file in the ",(0,t.jsx)(n.code,{children:"public/"})," directory and configure your ",(0,t.jsx)(n.code,{children:"SwaggerController"})," as follows:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nexport class OpenApiController extends SwaggerController {\n options = { url: '/openapi.yml' };\n}\n\n"})}),"\n",(0,t.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,t.jsx)(n.h3,{id:"using-controller-properties",children:"Using Controller Properties"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiRequestBody, IApiRequestBody, Post } from '@foal/core';\n\nclass ApiController {\n\n requestBody: IApiRequestBody = {\n content: {\n 'application/json': {\n schema: {\n type: 'object'\n }\n }\n },\n required: true\n };\n\n @Post('/products')\n // This is invalid:\n // @ApiRequestBody(this.requestBody)\n // This is valid:\n @ApiRequestBody(controller => controller.requestBody)\n createProduct() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"in-depth-overview",children:"In-Depth Overview"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"FoalTS automatically resolves the path items and operations based on your controller paths."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiResponse, Get, Post } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\nexport class ApiController {\n\n @Get('/products')\n @ApiResponse(200, { description: 'successful operation' })\n readProducts() {\n // ...\n }\n\n @Post('/products')\n @ApiResponse(200, { description: 'successful operation' })\n createProduct() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:'openapi: 3.0.0\ninfo:\n title: \'A Great API\'\n version: 1.0.0\npaths:\n /products: # Foal automatically puts the "get" and "post" operations under the same path item as required by OpenAPI rules.\n get:\n responses:\n 200:\n description: successful operation\n post:\n responses:\n 200:\n description: successful operation\n'})}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["The decorators ",(0,t.jsx)(n.code,{children:"@ApiServer"}),", ",(0,t.jsx)(n.code,{children:"@ApiSecurityRequirement"})," and ",(0,t.jsx)(n.code,{children:"@ApiExternalDocs"})," have a different behavior depending on if they decorate the root controller or a subcontroller / a method."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with the root controller"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiResponse, ApiServer } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n@ApiServer({ url: 'http://example.com' })\nexport class ApiController {\n\n // ...\n\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"openapi: 3.0.0\ninfo:\n title: 'A Great API'\n version: 1.0.0\npaths:\n # ...\nservers:\n- url: http://example.com\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a subcontroller / a method"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiResponse, ApiServer, Get } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\nexport class ApiController {\n\n @Get('/')\n @ApiServer({ url: 'http://example.com' })\n @ApiResponse(200, { description: 'successful operation' })\n index() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"openapi: 3.0.0\ninfo:\n title: 'A Great API'\n version: 1.0.0\npaths:\n /:\n get:\n responses:\n 200:\n description: successful operation\n servers:\n - url: http://example.com\n"})}),"\n",(0,t.jsx)(n.h3,{id:"define-and-reuse-components",children:"Define and Reuse Components"}),"\n",(0,t.jsx)(n.p,{children:"OpenAPI allows you to define and reuse components. Here is a way to achieve this with Foal."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ApiInfo, ApiDefineSchema, Get } from '@foal/core';\n\n@ApiInfo({\n title: 'A Great API',\n version: '1.0.0'\n})\n@ApiDefineSchema('product', {\n type: 'object',\n properties: {\n name: { type: 'string' }\n }\n required: [ 'name' ]\n})\nexport class ApiController {\n\n @Get('/products/:productId')\n @ApiResponse(200, {\n description: 'successful operation'\n content: {\n 'application/json': {\n schema: { $ref: '#/components/schemas/product' }\n }\n }\n })\n readProducts() {\n // ...\n }\n\n @Get('/products')\n @ApiResponse(200, {\n description: 'successful operation',\n content: {\n 'application/json': {\n schema: {\n type: 'array',\n items: { $ref: '#/components/schemas/product' }\n }\n }\n }\n })\n readProducts() {\n // ...\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.th,{children:"Component Decorators"})})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineSchema"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineResponse"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineParameter"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineExample"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineRequestBody"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineHeader"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineSecurityScheme"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineLink"})})}),(0,t.jsx)(n.tr,{children:(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"@ApiDefineCallback"})})})]})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"@ApiDefineXXX"})," decorators can be added to any controllers or methods but they always define components in the global scope of the API the controller belongs to."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The schemas defined with these decorators can also be re-used in the ",(0,t.jsx)(n.code,{children:"@ValidateXXX"})," hooks."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const productSchema = {\n // ...\n}\n\n@ApiDefineSchema('product', productSchema)\n@ValidateBody({\n $ref: '#/components/schemas/product'\n})\n"})}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"generate-and-save-a-specification-file-with-a-shell-script",children:"Generate and Save a Specification File with a Shell Script"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"foal generate script generate-openapi-doc\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/scripts/generate-openapi-doc.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { writeFileSync } from 'fs';\n\n// 3p\nimport { createOpenApiDocument } from '@foal/core';\nimport { stringify } from 'yamljs';\n\n// App\nimport { ApiController } from '../app/controllers';\n\nexport async function main() {\n const document = createOpenApiDocument(ApiController);\n const yamlDocument = stringify(document);\n\n writeFileSync('openapi.yml', yamlDocument, 'utf8');\n}\n\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run build\nfoal run generate-openapi-doc\n"})}),"\n",(0,t.jsx)(n.h3,{id:"common-errors",children:"Common Errors"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// ...\nexport class ApiController {\n @Get('/products/:id')\n getProduct() {\n return new HttpResponseOK();\n }\n\n @Put('/products/:productId')\n updateProduct() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"This example will throw this error."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Error: Templated paths with the same hierarchy but different templated names MUST NOT exist as they are identical.\n Path 1: /products/{id}\n Path 2: /products/{productId}\n"})}),"\n",(0,t.jsx)(n.p,{children:"OpenAPI does not support paths that are identical with different parameter names. Here is a way to solve this issue:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// ...\nexport class ApiController {\n @Get('/products/:productId')\n getProduct() {\n return new HttpResponseOK();\n }\n\n @Put('/products/:productId')\n updateProduct() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"extend-swagger-ui-options",children:"Extend Swagger UI options"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.a,{href:"https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/",children:"Swagger UI options"})," can be extended using the ",(0,t.jsx)(n.code,{children:"uiOptions"})," property."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\n\nimport { ApiController } from './api.controller';\n\nexport class OpenApiController extends SwaggerController {\n options = { controllerClass: ApiController };\n\n uiOptions = { docExpansion: 'full' };\n}\n\n"})})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},19365:(e,n,r)=>{r.d(n,{A:()=>i});r(96540);var t=r(34164);const o={tabItem:"tabItem_Ymn6"};var s=r(74848);function i(e){let{children:n,hidden:r,className:i}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,t.A)(o.tabItem,i),hidden:r,children:n})}},11470:(e,n,r)=>{r.d(n,{A:()=>b});var t=r(96540),o=r(34164),s=r(23104),i=r(56347),l=r(205),a=r(57485),c=r(31682),d=r(89466);function p(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return p(e).map((e=>{let{props:{value:n,label:r,attributes:t,default:o}}=e;return{value:n,label:r,attributes:t,default:o}}))}(r);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,r])}function u(e){let{value:n,tabValues:r}=e;return r.some((e=>e.value===n))}function m(e){let{queryString:n=!1,groupId:r}=e;const o=(0,i.W6)(),s=function(e){let{queryString:n=!1,groupId:r}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:n,groupId:r});return[(0,a.aZ)(s),(0,t.useCallback)((e=>{if(!s)return;const n=new URLSearchParams(o.location.search);n.set(s,e),o.replace({...o.location,search:n.toString()})}),[s,o])]}function x(e){const{defaultValue:n,queryString:r=!1,groupId:o}=e,s=h(e),[i,a]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!u({value:n,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=r.find((e=>e.default))??r[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:s}))),[c,p]=m({queryString:r,groupId:o}),[x,j]=function(e){let{groupId:n}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(n),[o,s]=(0,d.Dv)(r);return[o,(0,t.useCallback)((e=>{r&&s.set(e)}),[r,s])]}({groupId:o}),g=(()=>{const e=c??x;return u({value:e,tabValues:s})?e:null})();(0,l.A)((()=>{g&&a(g)}),[g]);return{selectedValue:i,selectValue:(0,t.useCallback)((e=>{if(!u({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);a(e),p(e),j(e)}),[p,j,s]),tabValues:s}}var j=r(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var f=r(74848);function A(e){let{className:n,block:r,selectedValue:t,selectValue:i,tabValues:l}=e;const a=[],{blockElementScrollPositionUntilNextRender:c}=(0,s.a_)(),d=e=>{const n=e.currentTarget,r=a.indexOf(n),o=l[r].value;o!==t&&(c(n),i(o))},p=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=a.indexOf(e.currentTarget)+1;n=a[r]??a[0];break}case"ArrowLeft":{const r=a.indexOf(e.currentTarget)-1;n=a[r]??a[a.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":r},n),children:l.map((e=>{let{value:n,label:r,attributes:s}=e;return(0,f.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>a.push(e),onKeyDown:p,onClick:d,...s,className:(0,o.A)("tabs__item",g.tabItem,s?.className,{"tabs__item--active":t===n}),children:r??n},n)}))})}function v(e){let{lazy:n,children:r,selectedValue:o}=e;const s=(Array.isArray(r)?r:[r]).filter(Boolean);if(n){const e=s.find((e=>e.props.value===o));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:s.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==o})))})}function y(e){const n=x(e);return(0,f.jsxs)("div",{className:(0,o.A)("tabs-container",g.tabList),children:[(0,f.jsx)(A,{...e,...n}),(0,f.jsx)(v,{...e,...n})]})}function b(e){const n=(0,j.A)();return(0,f.jsx)(y,{...e,children:p(e.children)},String(n))}},84886:(e,n,r)=>{r.d(n,{A:()=>t});const t=r.p+"assets/images/openapi-quick-start-4ec0b3bb97350bd038b2015be28a1381.png"},18559:(e,n,r)=>{r.d(n,{A:()=>t});const t=r.p+"assets/images/swagger-a641f2dfd065149a6d8c2e98ccad465e.png"},79252:(e,n,r)=>{r.d(n,{A:()=>t});const t=r.p+"assets/images/swagger3-3d35a6bde1ec1156853e65e4be372e6e.png"},28453:(e,n,r)=>{r.d(n,{R:()=>i,x:()=>l});var t=r(96540);const o={},s=t.createContext(o);function i(e){const n=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/09e23a09.e4d64ed9.js b/assets/js/09e23a09.4039317a.js similarity index 86% rename from assets/js/09e23a09.e4d64ed9.js rename to assets/js/09e23a09.4039317a.js index 57b657e71d..cebcba45fe 100644 --- a/assets/js/09e23a09.e4d64ed9.js +++ b/assets/js/09e23a09.4039317a.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9314],{52582:e=>{e.exports=JSON.parse('{"label":"release","permalink":"/blog/tags/release","allTagsPath":"/blog/tags","count":24,"unlisted":false}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9314],{52582:e=>{e.exports=JSON.parse('{"label":"release","permalink":"/blog/tags/release","allTagsPath":"/blog/tags","count":25,"unlisted":false}')}}]); \ No newline at end of file diff --git a/assets/js/0ee830f3.8e416081.js b/assets/js/0ee830f3.14746cdf.js similarity index 77% rename from assets/js/0ee830f3.8e416081.js rename to assets/js/0ee830f3.14746cdf.js index 2ca94ce89d..5acca28d74 100644 --- a/assets/js/0ee830f3.8e416081.js +++ b/assets/js/0ee830f3.14746cdf.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3633],{438:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>c});var n=i(74848),a=i(28453);const o={title:"Introduction",id:"tuto-1-introduction",slug:"1-introduction"},s=void 0,r={id:"tutorials/real-world-example-with-react/tuto-1-introduction",title:"Introduction",description:"This tutorial shows how to build a real-world application with React and Foal. It assumes that you have already read the first guide How to build a Simple To-Do List and that you have a basic knowledge of React.",source:"@site/docs/tutorials/real-world-example-with-react/1-introduction.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/1-introduction",permalink:"/docs/tutorials/real-world-example-with-react/1-introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/1-introduction.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Introduction",id:"tuto-1-introduction",slug:"1-introduction"},sidebar:"someSidebar",previous:{title:"Unit Testing",permalink:"/docs/tutorials/simple-todo-list/7-unit-testing"},next:{title:"Database Set Up",permalink:"/docs/tutorials/real-world-example-with-react/2-database-set-up"}},l={},c=[{value:"Application Overview",id:"application-overview",level:2},{value:"Get Started",id:"get-started",level:2}];function d(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,a.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["This tutorial shows how to build a real-world application with React and Foal. It assumes that you have already read the first guide ",(0,n.jsx)(t.em,{children:(0,n.jsx)(t.a,{href:"/docs/tutorials/simple-todo-list/1-installation",children:"How to build a Simple To-Do List"})})," and that you have a basic knowledge of React."]}),"\n",(0,n.jsx)(t.p,{children:"In this tutorial, you will learn to:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"establish a connection with MySQL or Postgres,"}),"\n",(0,n.jsx)(t.li,{children:"provide credentials to the application in a secure way,"}),"\n",(0,n.jsx)(t.li,{children:"create models with many-to-one relations,"}),"\n",(0,n.jsx)(t.li,{children:"use a query builder,"}),"\n",(0,n.jsx)(t.li,{children:"generate an interface to test your API (Swagger UI),"}),"\n",(0,n.jsx)(t.li,{children:"fix same-origin policy errors,"}),"\n",(0,n.jsx)(t.li,{children:"allow users to log in and register with an email and a password,"}),"\n",(0,n.jsx)(t.li,{children:"authenticate users on the frontend and the backend,"}),"\n",(0,n.jsx)(t.li,{children:"manage access control,"}),"\n",(0,n.jsx)(t.li,{children:"protect against CSRF attacks,"}),"\n",(0,n.jsx)(t.li,{children:"upload and save files,"}),"\n",(0,n.jsx)(t.li,{children:"allow users to connect with a social provider (Google),"}),"\n",(0,n.jsx)(t.li,{children:"and build the application for production."}),"\n"]}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsx)(t.p,{children:(0,n.jsxs)(t.em,{children:["For the sake of simplicity, the front-end application will not use a state management library (such as ",(0,n.jsx)(t.a,{href:"https://redux.js.org/",children:"redux"}),"). But you can of course add one if you wish. The logic to follow will remain mainly the same."]})}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"application-overview",children:"Application Overview"}),"\n",(0,n.jsx)(t.p,{children:"The application you will create is a social website where users can share interesting links to tutorials. All posts will be public, so no authentication will be required to view them. Publishing a post, on the other hand, will require the creation of an account."}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.em,{children:"Feed page"}),"\n",(0,n.jsx)(t.img,{alt:"Feed page",src:i(56823).A+"",width:"2560",height:"1452"})]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.em,{children:"Profile page"}),"\n",(0,n.jsx)(t.img,{alt:"Profile page",src:i(89012).A+"",width:"2560",height:"1450"})]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.em,{children:"Registration and login pages"}),"\n",(0,n.jsx)(t.img,{alt:"Registration and login pages",src:i(82049).A+"",width:"2560",height:"1448"})]}),"\n",(0,n.jsx)(t.h2,{id:"get-started",children:"Get Started"}),"\n",(0,n.jsx)(t.p,{children:"Let's get started. First of all, create a new directory."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"mkdir foal-react-tuto\n"})}),"\n",(0,n.jsx)(t.p,{children:"Generate the backend application."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"cd foal-react-tuto\nfoal createapp backend-app\n"})}),"\n",(0,n.jsx)(t.p,{children:"Then start the development server."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"cd backend-app\nnpm run dev\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Go to ",(0,n.jsx)(t.a,{href:"http://localhost:3001",children:"http://localhost:3001"})," in your browser. You should see the ",(0,n.jsx)(t.em,{children:"Welcome on board"})," message."]})]})}function h(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},56823:(e,t,i)=>{i.d(t,{A:()=>n});const n=i.p+"assets/images/feed-cafaeeea52a28612177a5a70e6c1cf12.png"},89012:(e,t,i)=>{i.d(t,{A:()=>n});const n=i.p+"assets/images/profile-d12409506e2332f8a2ef8391801ef85d.png"},82049:(e,t,i)=>{i.d(t,{A:()=>n});const n=i.p+"assets/images/sign-up-and-log-in-ea14815a2f012118eef7e058bd93c500.png"},28453:(e,t,i)=>{i.d(t,{R:()=>s,x:()=>r});var n=i(96540);const a={},o=n.createContext(a);function s(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3633],{438:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>c});var n=i(74848),a=i(28453);const o={title:"Introduction",id:"tuto-1-introduction",slug:"1-introduction"},s=void 0,r={id:"tutorials/real-world-example-with-react/tuto-1-introduction",title:"Introduction",description:"This tutorial shows how to build a real-world application with React and Foal. It assumes that you have already read the first guide How to build a Simple To-Do List and that you have a basic knowledge of React.",source:"@site/docs/tutorials/real-world-example-with-react/1-introduction.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/1-introduction",permalink:"/docs/tutorials/real-world-example-with-react/1-introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/1-introduction.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Introduction",id:"tuto-1-introduction",slug:"1-introduction"},sidebar:"someSidebar",previous:{title:"Unit Testing",permalink:"/docs/tutorials/simple-todo-list/7-unit-testing"},next:{title:"Database Set Up",permalink:"/docs/tutorials/real-world-example-with-react/2-database-set-up"}},l={},c=[{value:"Application Overview",id:"application-overview",level:2},{value:"Get Started",id:"get-started",level:2}];function d(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,a.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["This tutorial shows how to build a real-world application with React and Foal. It assumes that you have already read the first guide ",(0,n.jsx)(t.em,{children:(0,n.jsx)(t.a,{href:"/docs/tutorials/simple-todo-list/1-installation",children:"How to build a Simple To-Do List"})})," and that you have a basic knowledge of React."]}),"\n",(0,n.jsx)(t.p,{children:"In this tutorial, you will learn to:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"establish a connection with MySQL or Postgres,"}),"\n",(0,n.jsx)(t.li,{children:"provide credentials to the application in a secure way,"}),"\n",(0,n.jsx)(t.li,{children:"create models with many-to-one relations,"}),"\n",(0,n.jsx)(t.li,{children:"use a query builder,"}),"\n",(0,n.jsx)(t.li,{children:"generate an interface to test your API (Swagger UI),"}),"\n",(0,n.jsx)(t.li,{children:"fix same-origin policy errors,"}),"\n",(0,n.jsx)(t.li,{children:"allow users to log in and register with an email and a password,"}),"\n",(0,n.jsx)(t.li,{children:"authenticate users on the frontend and the backend,"}),"\n",(0,n.jsx)(t.li,{children:"manage access control,"}),"\n",(0,n.jsx)(t.li,{children:"protect against CSRF attacks,"}),"\n",(0,n.jsx)(t.li,{children:"upload and save files,"}),"\n",(0,n.jsx)(t.li,{children:"allow users to connect with a social provider (Google),"}),"\n",(0,n.jsx)(t.li,{children:"and build the application for production."}),"\n"]}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsx)(t.p,{children:(0,n.jsxs)(t.em,{children:["For the sake of simplicity, the front-end application will not use a state management library (such as ",(0,n.jsx)(t.a,{href:"https://redux.js.org/",children:"redux"}),"). But you can of course add one if you wish. The logic to follow will remain mainly the same."]})}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"application-overview",children:"Application Overview"}),"\n",(0,n.jsx)(t.p,{children:"The application you will create is a social website where users can share interesting links to tutorials. All posts will be public, so no authentication will be required to view them. Publishing a post, on the other hand, will require the creation of an account."}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.em,{children:"Feed page"}),"\n",(0,n.jsx)(t.img,{alt:"Feed page",src:i(56823).A+"",width:"2560",height:"1452"})]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.em,{children:"Profile page"}),"\n",(0,n.jsx)(t.img,{alt:"Profile page",src:i(89012).A+"",width:"2560",height:"1450"})]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.em,{children:"Registration and login pages"}),"\n",(0,n.jsx)(t.img,{alt:"Registration and login pages",src:i(82049).A+"",width:"2560",height:"1448"})]}),"\n",(0,n.jsx)(t.h2,{id:"get-started",children:"Get Started"}),"\n",(0,n.jsx)(t.p,{children:"Let's get started. First of all, create a new directory."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"mkdir foal-react-tuto\n"})}),"\n",(0,n.jsx)(t.p,{children:"Generate the backend application."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"cd foal-react-tuto\nnpx @foal/cli createapp backend-app\n"})}),"\n",(0,n.jsx)(t.p,{children:"Then start the development server."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"cd backend-app\nnpm run dev\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Go to ",(0,n.jsx)(t.a,{href:"http://localhost:3001",children:"http://localhost:3001"})," in your browser. You should see the ",(0,n.jsx)(t.em,{children:"Welcome on board"})," message."]})]})}function h(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},56823:(e,t,i)=>{i.d(t,{A:()=>n});const n=i.p+"assets/images/feed-cafaeeea52a28612177a5a70e6c1cf12.png"},89012:(e,t,i)=>{i.d(t,{A:()=>n});const n=i.p+"assets/images/profile-d12409506e2332f8a2ef8391801ef85d.png"},82049:(e,t,i)=>{i.d(t,{A:()=>n});const n=i.p+"assets/images/sign-up-and-log-in-ea14815a2f012118eef7e058bd93c500.png"},28453:(e,t,i)=>{i.d(t,{R:()=>s,x:()=>r});var n=i(96540);const a={},o=n.createContext(a);function s(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/15516f50.c5a7347b.js b/assets/js/15516f50.c5a7347b.js deleted file mode 100644 index 324da4a82b..0000000000 --- a/assets/js/15516f50.c5a7347b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1956],{25142:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>d});var t=s(74848),o=s(28453);const r={title:"Utilities"},i=void 0,a={id:"common/utilities",title:"Utilities",description:"Random Tokens",source:"@site/docs/common/utilities.md",sourceDirName:"common",slug:"/common/utilities",permalink:"/docs/common/utilities",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/utilities.md",tags:[],version:"current",frontMatter:{title:"Utilities"},sidebar:"someSidebar",previous:{title:"gRPC",permalink:"/docs/common/gRPC"},next:{title:"ExpressJS",permalink:"/docs/common/expressjs"}},c={},d=[{value:"Random Tokens",id:"random-tokens",level:2},{value:"Unsigned Tokens",id:"unsigned-tokens",level:3},{value:"Signed Tokens",id:"signed-tokens",level:3},{value:"String Encoding",id:"string-encoding",level:2},{value:"Base64 to Base64URL",id:"base64-to-base64url",level:3},{value:"Base64URL to Base64",id:"base64url-to-base64",level:3},{value:"Buffers & Streams",id:"buffers--streams",level:2},{value:"Stream to Buffer",id:"stream-to-buffer",level:3}];function l(e){const n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"random-tokens",children:"Random Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["In many situations, we need to generate tokens and then verify them. If your tokens are tied to a state (for example, a user ID), you should refer to the ",(0,t.jsx)(n.a,{href:"/docs/authentication/session-tokens",children:"sessions tokens"})," page. If not, the token generators below may be useful."]}),"\n",(0,t.jsx)(n.h3,{id:"unsigned-tokens",children:"Unsigned Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"generateToken"})," function generates a cryptographically secure random token encoded in base64url (128 bits)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { generateToken } from '@foal/core';\n\nconst token = await generateToken();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"signed-tokens",children:"Signed Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["You can also generate a token using a secret. The secret is used to ",(0,t.jsx)(n.em,{children:"sign"})," the token to provide extra security. It must be encoded in base64. You can generate one with the following command:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"foal createsecret\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Generate a signed token"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { generateSignedToken } from '@foal/core';\n\nconst token = await generateSignedToken(secret);\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Verify and read a signed token"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { verifySignedToken } from '@foal/core';\n\nconst signedTokenToVerify = 'xxx.yyy';\nconst result = await verifySignedToken(signedTokenToVerify, secret);\nif (result === false) {\n console.log('incorrect signature');\n} else {\n console.log('The token is ', result);\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"string-encoding",children:"String Encoding"}),"\n",(0,t.jsx)(n.h3,{id:"base64-to-base64url",children:"Base64 to Base64URL"}),"\n",(0,t.jsx)(n.p,{children:"This function converts a base64-encoded string into a base64URL-encoded string."}),"\n",(0,t.jsxs)(n.p,{children:["It replaces the characters ",(0,t.jsx)(n.code,{children:"+"})," and ",(0,t.jsx)(n.code,{children:"/"})," with ",(0,t.jsx)(n.code,{children:"-"})," and ",(0,t.jsx)(n.code,{children:"_"})," and omits the ",(0,t.jsx)(n.code,{children:"="})," sign."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { convertBase64ToBase64url } from '@foal/core';\n\nconst foo = convertBase64ToBase64url('bar');\n"})}),"\n",(0,t.jsx)(n.h3,{id:"base64url-to-base64",children:"Base64URL to Base64"}),"\n",(0,t.jsx)(n.p,{children:"This function converts a base64URL-encoded string into a base64-encoded string."}),"\n",(0,t.jsxs)(n.p,{children:["It replaces the characters ",(0,t.jsx)(n.code,{children:"-"})," and ",(0,t.jsx)(n.code,{children:"_"})," with ",(0,t.jsx)(n.code,{children:"+"})," and ",(0,t.jsx)(n.code,{children:"/"})," and adds the ",(0,t.jsx)(n.code,{children:"="})," padding character(s) if any."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { convertBase64urlToBase64 } from '@foal/core';\n\nconst foo = convertBase64urlToBase64('bar');\n"})}),"\n",(0,t.jsx)(n.h2,{id:"buffers--streams",children:"Buffers & Streams"}),"\n",(0,t.jsx)(n.h3,{id:"stream-to-buffer",children:"Stream to Buffer"}),"\n",(0,t.jsx)(n.p,{children:"This function converts a stream of buffers into a concatenated buffer. It returns a promise."}),"\n",(0,t.jsx)(n.p,{children:"If the stream emits an error, the promise is rejected with the emitted error."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { streamToBuffer } from '@foal/core';\n\nconst buffer = await streamToBuffer(stream);\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(l,{...e})}):l(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>i,x:()=>a});var t=s(96540);const o={},r=t.createContext(o);function i(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/15516f50.de5973ba.js b/assets/js/15516f50.de5973ba.js new file mode 100644 index 0000000000..b96cd08ef9 --- /dev/null +++ b/assets/js/15516f50.de5973ba.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1956],{25142:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>d});var t=s(74848),o=s(28453);const r={title:"Utilities"},i=void 0,a={id:"common/utilities",title:"Utilities",description:"Random Tokens",source:"@site/docs/common/utilities.md",sourceDirName:"common",slug:"/common/utilities",permalink:"/docs/common/utilities",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/utilities.md",tags:[],version:"current",frontMatter:{title:"Utilities"},sidebar:"someSidebar",previous:{title:"gRPC",permalink:"/docs/common/gRPC"},next:{title:"ExpressJS",permalink:"/docs/common/expressjs"}},c={},d=[{value:"Random Tokens",id:"random-tokens",level:2},{value:"Unsigned Tokens",id:"unsigned-tokens",level:3},{value:"Signed Tokens",id:"signed-tokens",level:3},{value:"String Encoding",id:"string-encoding",level:2},{value:"Base64 to Base64URL",id:"base64-to-base64url",level:3},{value:"Base64URL to Base64",id:"base64url-to-base64",level:3},{value:"Buffers & Streams",id:"buffers--streams",level:2},{value:"Stream to Buffer",id:"stream-to-buffer",level:3}];function l(e){const n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"random-tokens",children:"Random Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["In many situations, we need to generate tokens and then verify them. If your tokens are tied to a state (for example, a user ID), you should refer to the ",(0,t.jsx)(n.a,{href:"/docs/authentication/session-tokens",children:"sessions tokens"})," page. If not, the token generators below may be useful."]}),"\n",(0,t.jsx)(n.h3,{id:"unsigned-tokens",children:"Unsigned Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"generateToken"})," function generates a cryptographically secure random token encoded in base64url (128 bits)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { generateToken } from '@foal/core';\n\nconst token = await generateToken();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"signed-tokens",children:"Signed Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["You can also generate a token using a secret. The secret is used to ",(0,t.jsx)(n.em,{children:"sign"})," the token to provide extra security. It must be encoded in base64. You can generate one with the following command:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npx foal createsecret\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Generate a signed token"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { generateSignedToken } from '@foal/core';\n\nconst token = await generateSignedToken(secret);\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Verify and read a signed token"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { verifySignedToken } from '@foal/core';\n\nconst signedTokenToVerify = 'xxx.yyy';\nconst result = await verifySignedToken(signedTokenToVerify, secret);\nif (result === false) {\n console.log('incorrect signature');\n} else {\n console.log('The token is ', result);\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"string-encoding",children:"String Encoding"}),"\n",(0,t.jsx)(n.h3,{id:"base64-to-base64url",children:"Base64 to Base64URL"}),"\n",(0,t.jsx)(n.p,{children:"This function converts a base64-encoded string into a base64URL-encoded string."}),"\n",(0,t.jsxs)(n.p,{children:["It replaces the characters ",(0,t.jsx)(n.code,{children:"+"})," and ",(0,t.jsx)(n.code,{children:"/"})," with ",(0,t.jsx)(n.code,{children:"-"})," and ",(0,t.jsx)(n.code,{children:"_"})," and omits the ",(0,t.jsx)(n.code,{children:"="})," sign."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { convertBase64ToBase64url } from '@foal/core';\n\nconst foo = convertBase64ToBase64url('bar');\n"})}),"\n",(0,t.jsx)(n.h3,{id:"base64url-to-base64",children:"Base64URL to Base64"}),"\n",(0,t.jsx)(n.p,{children:"This function converts a base64URL-encoded string into a base64-encoded string."}),"\n",(0,t.jsxs)(n.p,{children:["It replaces the characters ",(0,t.jsx)(n.code,{children:"-"})," and ",(0,t.jsx)(n.code,{children:"_"})," with ",(0,t.jsx)(n.code,{children:"+"})," and ",(0,t.jsx)(n.code,{children:"/"})," and adds the ",(0,t.jsx)(n.code,{children:"="})," padding character(s) if any."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { convertBase64urlToBase64 } from '@foal/core';\n\nconst foo = convertBase64urlToBase64('bar');\n"})}),"\n",(0,t.jsx)(n.h2,{id:"buffers--streams",children:"Buffers & Streams"}),"\n",(0,t.jsx)(n.h3,{id:"stream-to-buffer",children:"Stream to Buffer"}),"\n",(0,t.jsx)(n.p,{children:"This function converts a stream of buffers into a concatenated buffer. It returns a promise."}),"\n",(0,t.jsx)(n.p,{children:"If the stream emits an error, the promise is rejected with the emitted error."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { streamToBuffer } from '@foal/core';\n\nconst buffer = await streamToBuffer(stream);\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(l,{...e})}):l(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>i,x:()=>a});var t=s(96540);const o={},r=t.createContext(o);function i(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/163c81f1.91d88524.js b/assets/js/163c81f1.91d88524.js new file mode 100644 index 0000000000..a49fdadd95 --- /dev/null +++ b/assets/js/163c81f1.91d88524.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4994],{96368:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var t=o(74848),s=o(28453);const r={title:"Hooks"},i=void 0,c={id:"architecture/hooks",title:"Hooks",description:"Description",source:"@site/docs/architecture/hooks.md",sourceDirName:"architecture",slug:"/architecture/hooks",permalink:"/docs/architecture/hooks",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/hooks.md",tags:[],version:"current",frontMatter:{title:"Hooks"},sidebar:"someSidebar",previous:{title:"Services & Dependency Injection",permalink:"/docs/architecture/services-and-dependency-injection"},next:{title:"Initialization",permalink:"/docs/architecture/initialization"}},a={},l=[{value:"Description",id:"description",level:2},{value:"Built-in Hooks",id:"built-in-hooks",level:2},{value:"Use",id:"use",level:2},{value:"Build Custom Hooks",id:"build-custom-hooks",level:2},{value:"Executing Logic After the Controller Method",id:"executing-logic-after-the-controller-method",level:3},{value:"Grouping Several Hooks into One",id:"grouping-several-hooks-into-one",level:2},{value:"Testing Hooks",id:"testing-hooks",level:2},{value:"Testing Hook Post Functions",id:"testing-hook-post-functions",level:3},{value:"Testing Hooks that Use this",id:"testing-hooks-that-use-this",level:3},{value:"Mocking services",id:"mocking-services",level:3},{value:"Hook factories",id:"hook-factories",level:2},{value:"Forward Data Between Hooks",id:"forward-data-between-hooks",level:2}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"npx foal generate hook my-hook\n"})}),"\n",(0,t.jsx)(n.h2,{id:"description",children:"Description"}),"\n",(0,t.jsx)(n.p,{children:"Hooks are decorators that execute extra logic before and/or after the execution of a controller method."}),"\n",(0,t.jsx)(n.p,{children:"They are particulary useful in these scenarios:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"authentication & access control"}),"\n",(0,t.jsx)(n.li,{children:"request validation & sanitization"}),"\n",(0,t.jsx)(n.li,{children:"logging"}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"They improve code readability and make unit testing easier."}),"\n",(0,t.jsx)(n.h2,{id:"built-in-hooks",children:"Built-in Hooks"}),"\n",(0,t.jsx)(n.p,{children:"Foal provides a number of hooks to handle the most common scenarios."}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ValidateBody"}),", ",(0,t.jsx)(n.code,{children:"ValidateHeader"}),", ",(0,t.jsx)(n.code,{children:"ValidatePathParam"}),", ",(0,t.jsx)(n.code,{children:"ValidateCookie"})," and ",(0,t.jsx)(n.code,{children:"ValidateQueryParam"})," validate the format of the incoming HTTP requests (see ",(0,t.jsx)(n.a,{href:"/docs/common/validation-and-sanitization",children:"Validation"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"Log"})," displays information on the request (see ",(0,t.jsx)(n.a,{href:"/docs/common/logging",children:"Logging"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"JWTRequired"}),", ",(0,t.jsx)(n.code,{children:"JWTOptional"}),", ",(0,t.jsx)(n.code,{children:"UseSessions"})," authenticate the user by filling the ",(0,t.jsx)(n.code,{children:"ctx.user"})," property."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"PermissionRequired"})," restricts the route access to certain users."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"use",children:"Use"}),"\n",(0,t.jsx)(n.p,{children:"A hook can decorate a controller method or the controller itself. If it decorates the controller then it applies to all its methods and sub-controllers."}),"\n",(0,t.jsxs)(n.p,{children:["In the below example, ",(0,t.jsx)(n.code,{children:"JWTRequired"})," applies to ",(0,t.jsx)(n.code,{children:"listProducts"})," and ",(0,t.jsx)(n.code,{children:"addProduct"}),"."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import {\n Context, Get, HttpResponseCreated, HttpResponseOK, Post, ValidateBody\n} from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired()\nclass AppController {\n private products = [\n { name: 'Hoover' }\n ];\n\n @Get('/products')\n listProducts() {\n return new HttpResponseOK(this.products);\n }\n\n @Post('/products')\n @ValidateBody({\n additionalProperties: false,\n properties: {\n name: { type: 'string' }\n },\n required: [ 'name' ],\n type: 'object',\n })\n addProduct(ctx: Context) {\n this.products.push(ctx.request.body);\n return new HttpResponseCreated();\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If the user makes a POST request to ",(0,t.jsx)(n.code,{children:"/products"})," whereas she/he is not authenticated, then the server will respond with a 400 error and the ",(0,t.jsx)(n.code,{children:"ValidateBody"})," hook and ",(0,t.jsx)(n.code,{children:"addProduct"})," method won't be executed."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If you need to apply a hook globally, you just have to make it decorate the root controller: ",(0,t.jsx)(n.code,{children:"AppController"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@Log('Request body:', { body: true })\nexport class AppController {\n // ...\n}\n"})}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"build-custom-hooks",children:"Build Custom Hooks"}),"\n",(0,t.jsx)(n.p,{children:"In addition to the hooks provided by FoalTS, you can also create your own."}),"\n",(0,t.jsx)(n.p,{children:"A hook is made of a small function, synchronous or asynchronous, that is executed before the controller method."}),"\n",(0,t.jsxs)(n.p,{children:["To create one, you need to call the ",(0,t.jsx)(n.code,{children:"Hook"})," function."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass MyController {\n\n @Get('/')\n @Hook(() => {\n console.log('Receiving GET / request...');\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The hook function can take two parameters: the ",(0,t.jsx)(n.code,{children:"Context"})," object and the service manager. The ",(0,t.jsx)(n.a,{href:"/docs/architecture/controllers",children:"Context object"})," is specific to the request and gives you information on it. The service manager lets you access any service through its ",(0,t.jsx)(n.code,{children:"get"})," method."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass Logger {\n log(message: string) {\n console.log(`${new Date()} - ${message}`);\n }\n}\n\nclass MyController {\n\n @Get('/')\n @Hook((ctx, services) => {\n const logger = services.get(Logger);\n logger.log('IP: ' + ctx.request.ip);\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["A hook function can return an ",(0,t.jsx)(n.code,{children:"HttpResponse"})," object. If so, the remaining hooks and the controller method are not executed and the object is used to render the HTTP response."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Hook, HttpResponseBadRequest, HttpResponseOK, Post } from '@foal/core';\n\nclass MyController {\n\n @Post('/')\n @Hook((ctx: Context) => {\n if (typeof ctx.request.body.name !== 'string') {\n return new HttpResponseBadRequest();\n }\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can also have access to the controller instance through the ",(0,t.jsx)(n.code,{children:"this"})," keyword."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { getAjvInstance, Hook, HttpResponseBadRequest, HttpResponseOK, Post } from '@foal/core';\n\nclass MyController {\n\n schema = {\n properties: {\n price: { type: 'number' }\n },\n type: 'object',\n };\n\n @Post('/')\n @Hook(function (this: MyController, ctx, services) {\n const ajv = getAjvInstance();\n const requestBody = ctx.request.body;\n if (!ajv.validate(this.schema, requestBody)) {\n return new HttpResponseBadRequest(ajv.errors);\n }\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"executing-logic-after-the-controller-method",children:"Executing Logic After the Controller Method"}),"\n",(0,t.jsxs)(n.p,{children:["A hook can also be used to execute extra logic after the controller method. To do so, you can return a ",(0,t.jsx)(n.em,{children:"hook post function"})," inside the hook. This function will be executed after the controller method. It takes exactly one parameter: the ",(0,t.jsx)(n.code,{children:"HttpResponse"})," object returned by the controller."]}),"\n",(0,t.jsx)(n.p,{children:"The below example shows how to add a custom cookie to the response returned by the controller."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass MyController {\n\n @Get('/')\n @Hook(() => response => {\n response.setCookie('X-CSRF-TOKEN', 'xxx');\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"This example shows how to execute logic both before and after the controller method."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass MyController {\n\n @Get('/')\n @Hook(() => {\n const time = process.hrtime();\n\n return () => {\n const seconds = process.hrtime(time)[0];\n console.log(`Executed in ${seconds} seconds`);\n };\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"grouping-several-hooks-into-one",children:"Grouping Several Hooks into One"}),"\n",(0,t.jsxs)(n.p,{children:["In case you need to group several hooks together, the ",(0,t.jsx)(n.code,{children:"MergeHooks"})," function can be used to do this."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, MergeHooks, ValidateCookie, ValidateHeader } from '@foal/core';\n\n// Before\n\nclass MyController {\n @Get('/products')\n @ValidateHeader('Authorization')\n @ValidateCookie('foo')\n readProducts() {\n return new HttpResponseOK();\n }\n}\n\n// After\n\nfunction ValidateAll() {\n return MergeHooks(\n ValidateHeader('Authorization'),\n ValidateCookie('foo')\n );\n}\n\nclass MyController {\n @Get('/products')\n @ValidateAll()\n readProducts() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"testing-hooks",children:"Testing Hooks"}),"\n",(0,t.jsxs)(n.p,{children:["Hooks can be tested thanks to the utility ",(0,t.jsx)(n.code,{children:"getHookFunction"})," (or ",(0,t.jsx)(n.code,{children:"getHookFunctions"})," if the hook was made from several functions)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-body.hook.ts\nimport { Hook, HttpResponseBadRequest } from '@foal/core';\n\nexport function ValidateBody() {\n return Hook(ctx => {\n if (typeof ctx.request.body.name !== 'string') {\n return new HttpResponseBadRequest();\n }\n });\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-body.hook.spec.ts\nimport {\n Context, getHookFunction,\n isHttpResponseBadRequest, ServiceManager\n} from '@foal/core';\nimport { ValidateBody } from './validate-body.hook';\n\nit('ValidateBody', () => {\n const ctx = new Context({\n // fake request object\n body: { name: 3 }\n });\n const hook = getHookFunction(ValidateBody());\n \n const response = hook(ctx, new ServiceManager());\n\n if (!isHttpResponseBadRequest(response)) {\n throw new Error('The hook should return an HttpResponseBadRequest object.');\n }\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"testing-hook-post-functions",children:"Testing Hook Post Functions"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// add-xxx-header.hook.ts\nimport { Hook } from '@foal/core';\n\nexport function AddXXXHeader() {\n return Hook(ctx => response => {\n response.setHeader('XXX', 'YYY');\n });\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// add-xxx-header.hook.spec.ts\nimport { strictEqual } from 'assert';\nimport {\n Context, getHookFunction, HttpResponseOK,\n isHttpResponse, ServiceManager\n} from '@foal/core';\nimport { AddXXXHeader } from './add-xxx-header.hook';\n\nit('AddXXXHeader', async () => {\n const ctx = new Context({});\n const hook = getHookFunction(AddXXXHeader());\n \n const postHookFunction = await hook(ctx, new ServiceManager());\n if (postHookFunction === undefined || isHttpResponse(postHookFunction)) {\n throw new Error('The hook should return a post hook function');\n }\n\n const response = new HttpResponseOK();\n await postHookFunction(response);\n\n strictEqual(response.getHeader('XXX'), 'YYY');\n});\n"})}),"\n",(0,t.jsxs)(n.h3,{id:"testing-hooks-that-use-this",children:["Testing Hooks that Use ",(0,t.jsx)(n.code,{children:"this"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-param-type.hook.ts\nimport { Context, Hook, HttpResponseBadRequest } from '@foal/core';\n\nexport function ValidateParamType() {\n return Hook(function(this: any, ctx: Context) {\n if (typeof ctx.request.params.id !== this.paramType) {\n return new HttpResponseBadRequest();\n }\n });\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-param-type.hook.spec.ts\nimport { Context, getHookFunction, HttpResponseBadRequest } from '@foal/core';\nimport { ValidateParamType } from './validate-param-type';\n\nit('ValidateParamType', () => {\n const ctx = new Context({\n // fake request object\n params: { id: 'xxx' }\n });\n const controller = {\n paramType: 'number'\n };\n const hook = getHookFunction(ValidateParamType()).bind(controller);\n\n const response = hook(ctx, new ServiceManager());\n\n if (!isHttpResponseBadRequest(response)) {\n throw new Error('The hook should return an HttpResponseBadRequest object.');\n }\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"mocking-services",children:"Mocking services"}),"\n",(0,t.jsxs)(n.p,{children:["You can mock services by using the ",(0,t.jsx)(n.code,{children:"set"})," method of the service manager."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// authenticate.hook.ts\nimport { Hook } from '@foal/core';\n\nexport class UserService {\n private users: any = {\n eh4sb: { id: 1, name: 'John' },\n kadu5: { id: 2, name: 'Mary' }\n };\n\n getUser(key: string) {\n return this.users[key] ?? null;\n }\n}\n\nexport const authenticate = Hook((ctx, services) => {\n const users = services.get(UserService);\n ctx.user = users.getUser(ctx.request.params.key);\n});\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// authenticate.hook.spec.ts\nimport { strictEqual } from 'assert';\nimport { Context, getHookFunction, ServiceManager } from '@foal/core';\nimport { authenticate, UserService } from './authenticate.hook';\n\nit('authenticate', () => {\n const hook = getHookFunction(authenticate);\n\n const user = { id: 3, name: 'Bob' };\n\n new Context({ params: { key: 'xxx' }});\n const services = new ServiceManager();\n services.set(UserService, {\n getUser() {\n return user;\n }\n })\n \n hook(ctx, services);\n\n strictEqual(ctx.user, user);\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"hook-factories",children:"Hook factories"}),"\n",(0,t.jsx)(n.p,{children:"Usually, we don't create hooks directly but with hook factories. Thus it is easier to customize the hook behavior on each route."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nfunction Log(msg: string) {\n return Hook(() => { console.log(msg); });\n}\n\nclass MyController {\n @Get('/route1')\n @Log('Receiving a GET /route1 request...')\n route1() {\n return new HttpResponseOK('Hello world!');\n }\n\n @Get('/route2')\n @Log('Receiving a GET /route2 request...')\n route2() {\n return new HttpResponseOK('Hello world!');\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"forward-data-between-hooks",children:"Forward Data Between Hooks"}),"\n",(0,t.jsxs)(n.p,{children:["If you need to transfer data from one hook to another or to the controller method, you can use the ",(0,t.jsx)(n.code,{children:"state"})," property of the ",(0,t.jsx)(n.code,{children:"Context"})," object to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, Hook, HttpResponseOK, UserRequired } from '@foal/core';\nimport { Org } from '../entities';\n\nfunction AddOrgToContext() {\n return Hook(async ctx => {\n if (ctx.user) {\n ctx.state.org = await Org.findOneByOrFail({ id: ctx.user.orgId });\n }\n })\n}\n\nexport class ApiController {\n\n @Get('/org-name')\n @UserRequired()\n @AddOrgToContext()\n readOrgName(ctx: Context) {\n return new HttpResponseOK(ctx.state.org.name);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"If needed, you can also define an interface for your state and pass it as type argument to the context."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"interface State {\n org: Org;\n}\n\nexport class ApiController {\n // ...\n readOrgName(ctx: Context) {\n // ...\n }\n}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},28453:(e,n,o)=>{o.d(n,{R:()=>i,x:()=>c});var t=o(96540);const s={},r=t.createContext(s);function i(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/163c81f1.9479235b.js b/assets/js/163c81f1.9479235b.js deleted file mode 100644 index d15c2dd24b..0000000000 --- a/assets/js/163c81f1.9479235b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4994],{96368:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var t=o(74848),s=o(28453);const r={title:"Hooks"},i=void 0,c={id:"architecture/hooks",title:"Hooks",description:"Description",source:"@site/docs/architecture/hooks.md",sourceDirName:"architecture",slug:"/architecture/hooks",permalink:"/docs/architecture/hooks",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/hooks.md",tags:[],version:"current",frontMatter:{title:"Hooks"},sidebar:"someSidebar",previous:{title:"Services & Dependency Injection",permalink:"/docs/architecture/services-and-dependency-injection"},next:{title:"Initialization",permalink:"/docs/architecture/initialization"}},a={},l=[{value:"Description",id:"description",level:2},{value:"Built-in Hooks",id:"built-in-hooks",level:2},{value:"Use",id:"use",level:2},{value:"Build Custom Hooks",id:"build-custom-hooks",level:2},{value:"Executing Logic After the Controller Method",id:"executing-logic-after-the-controller-method",level:3},{value:"Grouping Several Hooks into One",id:"grouping-several-hooks-into-one",level:2},{value:"Testing Hooks",id:"testing-hooks",level:2},{value:"Testing Hook Post Functions",id:"testing-hook-post-functions",level:3},{value:"Testing Hooks that Use this",id:"testing-hooks-that-use-this",level:3},{value:"Mocking services",id:"mocking-services",level:3},{value:"Hook factories",id:"hook-factories",level:2},{value:"Forward Data Between Hooks",id:"forward-data-between-hooks",level:2}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"foal generate hook my-hook\n"})}),"\n",(0,t.jsx)(n.h2,{id:"description",children:"Description"}),"\n",(0,t.jsx)(n.p,{children:"Hooks are decorators that execute extra logic before and/or after the execution of a controller method."}),"\n",(0,t.jsx)(n.p,{children:"They are particulary useful in these scenarios:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"authentication & access control"}),"\n",(0,t.jsx)(n.li,{children:"request validation & sanitization"}),"\n",(0,t.jsx)(n.li,{children:"logging"}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"They improve code readability and make unit testing easier."}),"\n",(0,t.jsx)(n.h2,{id:"built-in-hooks",children:"Built-in Hooks"}),"\n",(0,t.jsx)(n.p,{children:"Foal provides a number of hooks to handle the most common scenarios."}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"ValidateBody"}),", ",(0,t.jsx)(n.code,{children:"ValidateHeader"}),", ",(0,t.jsx)(n.code,{children:"ValidatePathParam"}),", ",(0,t.jsx)(n.code,{children:"ValidateCookie"})," and ",(0,t.jsx)(n.code,{children:"ValidateQueryParam"})," validate the format of the incoming HTTP requests (see ",(0,t.jsx)(n.a,{href:"/docs/common/validation-and-sanitization",children:"Validation"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"Log"})," displays information on the request (see ",(0,t.jsx)(n.a,{href:"/docs/common/logging",children:"Logging"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"JWTRequired"}),", ",(0,t.jsx)(n.code,{children:"JWTOptional"}),", ",(0,t.jsx)(n.code,{children:"UseSessions"})," authenticate the user by filling the ",(0,t.jsx)(n.code,{children:"ctx.user"})," property."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"PermissionRequired"})," restricts the route access to certain users."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"use",children:"Use"}),"\n",(0,t.jsx)(n.p,{children:"A hook can decorate a controller method or the controller itself. If it decorates the controller then it applies to all its methods and sub-controllers."}),"\n",(0,t.jsxs)(n.p,{children:["In the below example, ",(0,t.jsx)(n.code,{children:"JWTRequired"})," applies to ",(0,t.jsx)(n.code,{children:"listProducts"})," and ",(0,t.jsx)(n.code,{children:"addProduct"}),"."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import {\n Context, Get, HttpResponseCreated, HttpResponseOK, Post, ValidateBody\n} from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired()\nclass AppController {\n private products = [\n { name: 'Hoover' }\n ];\n\n @Get('/products')\n listProducts() {\n return new HttpResponseOK(this.products);\n }\n\n @Post('/products')\n @ValidateBody({\n additionalProperties: false,\n properties: {\n name: { type: 'string' }\n },\n required: [ 'name' ],\n type: 'object',\n })\n addProduct(ctx: Context) {\n this.products.push(ctx.request.body);\n return new HttpResponseCreated();\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If the user makes a POST request to ",(0,t.jsx)(n.code,{children:"/products"})," whereas she/he is not authenticated, then the server will respond with a 400 error and the ",(0,t.jsx)(n.code,{children:"ValidateBody"})," hook and ",(0,t.jsx)(n.code,{children:"addProduct"})," method won't be executed."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If you need to apply a hook globally, you just have to make it decorate the root controller: ",(0,t.jsx)(n.code,{children:"AppController"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@Log('Request body:', { body: true })\nexport class AppController {\n // ...\n}\n"})}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"build-custom-hooks",children:"Build Custom Hooks"}),"\n",(0,t.jsx)(n.p,{children:"In addition to the hooks provided by FoalTS, you can also create your own."}),"\n",(0,t.jsx)(n.p,{children:"A hook is made of a small function, synchronous or asynchronous, that is executed before the controller method."}),"\n",(0,t.jsxs)(n.p,{children:["To create one, you need to call the ",(0,t.jsx)(n.code,{children:"Hook"})," function."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass MyController {\n\n @Get('/')\n @Hook(() => {\n console.log('Receiving GET / request...');\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The hook function can take two parameters: the ",(0,t.jsx)(n.code,{children:"Context"})," object and the service manager. The ",(0,t.jsx)(n.a,{href:"/docs/architecture/controllers",children:"Context object"})," is specific to the request and gives you information on it. The service manager lets you access any service through its ",(0,t.jsx)(n.code,{children:"get"})," method."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass Logger {\n log(message: string) {\n console.log(`${new Date()} - ${message}`);\n }\n}\n\nclass MyController {\n\n @Get('/')\n @Hook((ctx, services) => {\n const logger = services.get(Logger);\n logger.log('IP: ' + ctx.request.ip);\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["A hook function can return an ",(0,t.jsx)(n.code,{children:"HttpResponse"})," object. If so, the remaining hooks and the controller method are not executed and the object is used to render the HTTP response."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Hook, HttpResponseBadRequest, HttpResponseOK, Post } from '@foal/core';\n\nclass MyController {\n\n @Post('/')\n @Hook((ctx: Context) => {\n if (typeof ctx.request.body.name !== 'string') {\n return new HttpResponseBadRequest();\n }\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can also have access to the controller instance through the ",(0,t.jsx)(n.code,{children:"this"})," keyword."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { getAjvInstance, Hook, HttpResponseBadRequest, HttpResponseOK, Post } from '@foal/core';\n\nclass MyController {\n\n schema = {\n properties: {\n price: { type: 'number' }\n },\n type: 'object',\n };\n\n @Post('/')\n @Hook(function (this: MyController, ctx, services) {\n const ajv = getAjvInstance();\n const requestBody = ctx.request.body;\n if (!ajv.validate(this.schema, requestBody)) {\n return new HttpResponseBadRequest(ajv.errors);\n }\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"executing-logic-after-the-controller-method",children:"Executing Logic After the Controller Method"}),"\n",(0,t.jsxs)(n.p,{children:["A hook can also be used to execute extra logic after the controller method. To do so, you can return a ",(0,t.jsx)(n.em,{children:"hook post function"})," inside the hook. This function will be executed after the controller method. It takes exactly one parameter: the ",(0,t.jsx)(n.code,{children:"HttpResponse"})," object returned by the controller."]}),"\n",(0,t.jsx)(n.p,{children:"The below example shows how to add a custom cookie to the response returned by the controller."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass MyController {\n\n @Get('/')\n @Hook(() => response => {\n response.setCookie('X-CSRF-TOKEN', 'xxx');\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"This example shows how to execute logic both before and after the controller method."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nclass MyController {\n\n @Get('/')\n @Hook(() => {\n const time = process.hrtime();\n\n return () => {\n const seconds = process.hrtime(time)[0];\n console.log(`Executed in ${seconds} seconds`);\n };\n })\n index() {\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"grouping-several-hooks-into-one",children:"Grouping Several Hooks into One"}),"\n",(0,t.jsxs)(n.p,{children:["In case you need to group several hooks together, the ",(0,t.jsx)(n.code,{children:"MergeHooks"})," function can be used to do this."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, MergeHooks, ValidateCookie, ValidateHeader } from '@foal/core';\n\n// Before\n\nclass MyController {\n @Get('/products')\n @ValidateHeader('Authorization')\n @ValidateCookie('foo')\n readProducts() {\n return new HttpResponseOK();\n }\n}\n\n// After\n\nfunction ValidateAll() {\n return MergeHooks(\n ValidateHeader('Authorization'),\n ValidateCookie('foo')\n );\n}\n\nclass MyController {\n @Get('/products')\n @ValidateAll()\n readProducts() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"testing-hooks",children:"Testing Hooks"}),"\n",(0,t.jsxs)(n.p,{children:["Hooks can be tested thanks to the utility ",(0,t.jsx)(n.code,{children:"getHookFunction"})," (or ",(0,t.jsx)(n.code,{children:"getHookFunctions"})," if the hook was made from several functions)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-body.hook.ts\nimport { Hook, HttpResponseBadRequest } from '@foal/core';\n\nexport function ValidateBody() {\n return Hook(ctx => {\n if (typeof ctx.request.body.name !== 'string') {\n return new HttpResponseBadRequest();\n }\n });\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-body.hook.spec.ts\nimport {\n Context, getHookFunction,\n isHttpResponseBadRequest, ServiceManager\n} from '@foal/core';\nimport { ValidateBody } from './validate-body.hook';\n\nit('ValidateBody', () => {\n const ctx = new Context({\n // fake request object\n body: { name: 3 }\n });\n const hook = getHookFunction(ValidateBody());\n \n const response = hook(ctx, new ServiceManager());\n\n if (!isHttpResponseBadRequest(response)) {\n throw new Error('The hook should return an HttpResponseBadRequest object.');\n }\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"testing-hook-post-functions",children:"Testing Hook Post Functions"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// add-xxx-header.hook.ts\nimport { Hook } from '@foal/core';\n\nexport function AddXXXHeader() {\n return Hook(ctx => response => {\n response.setHeader('XXX', 'YYY');\n });\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// add-xxx-header.hook.spec.ts\nimport { strictEqual } from 'assert';\nimport {\n Context, getHookFunction, HttpResponseOK,\n isHttpResponse, ServiceManager\n} from '@foal/core';\nimport { AddXXXHeader } from './add-xxx-header.hook';\n\nit('AddXXXHeader', async () => {\n const ctx = new Context({});\n const hook = getHookFunction(AddXXXHeader());\n \n const postHookFunction = await hook(ctx, new ServiceManager());\n if (postHookFunction === undefined || isHttpResponse(postHookFunction)) {\n throw new Error('The hook should return a post hook function');\n }\n\n const response = new HttpResponseOK();\n await postHookFunction(response);\n\n strictEqual(response.getHeader('XXX'), 'YYY');\n});\n"})}),"\n",(0,t.jsxs)(n.h3,{id:"testing-hooks-that-use-this",children:["Testing Hooks that Use ",(0,t.jsx)(n.code,{children:"this"})]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-param-type.hook.ts\nimport { Context, Hook, HttpResponseBadRequest } from '@foal/core';\n\nexport function ValidateParamType() {\n return Hook(function(this: any, ctx: Context) {\n if (typeof ctx.request.params.id !== this.paramType) {\n return new HttpResponseBadRequest();\n }\n });\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// validate-param-type.hook.spec.ts\nimport { Context, getHookFunction, HttpResponseBadRequest } from '@foal/core';\nimport { ValidateParamType } from './validate-param-type';\n\nit('ValidateParamType', () => {\n const ctx = new Context({\n // fake request object\n params: { id: 'xxx' }\n });\n const controller = {\n paramType: 'number'\n };\n const hook = getHookFunction(ValidateParamType()).bind(controller);\n\n const response = hook(ctx, new ServiceManager());\n\n if (!isHttpResponseBadRequest(response)) {\n throw new Error('The hook should return an HttpResponseBadRequest object.');\n }\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"mocking-services",children:"Mocking services"}),"\n",(0,t.jsxs)(n.p,{children:["You can mock services by using the ",(0,t.jsx)(n.code,{children:"set"})," method of the service manager."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// authenticate.hook.ts\nimport { Hook } from '@foal/core';\n\nexport class UserService {\n private users: any = {\n eh4sb: { id: 1, name: 'John' },\n kadu5: { id: 2, name: 'Mary' }\n };\n\n getUser(key: string) {\n return this.users[key] ?? null;\n }\n}\n\nexport const authenticate = Hook((ctx, services) => {\n const users = services.get(UserService);\n ctx.user = users.getUser(ctx.request.params.key);\n});\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// authenticate.hook.spec.ts\nimport { strictEqual } from 'assert';\nimport { Context, getHookFunction, ServiceManager } from '@foal/core';\nimport { authenticate, UserService } from './authenticate.hook';\n\nit('authenticate', () => {\n const hook = getHookFunction(authenticate);\n\n const user = { id: 3, name: 'Bob' };\n\n new Context({ params: { key: 'xxx' }});\n const services = new ServiceManager();\n services.set(UserService, {\n getUser() {\n return user;\n }\n })\n \n hook(ctx, services);\n\n strictEqual(ctx.user, user);\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"hook-factories",children:"Hook factories"}),"\n",(0,t.jsx)(n.p,{children:"Usually, we don't create hooks directly but with hook factories. Thus it is easier to customize the hook behavior on each route."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, Hook, HttpResponseOK } from '@foal/core';\n\nfunction Log(msg: string) {\n return Hook(() => { console.log(msg); });\n}\n\nclass MyController {\n @Get('/route1')\n @Log('Receiving a GET /route1 request...')\n route1() {\n return new HttpResponseOK('Hello world!');\n }\n\n @Get('/route2')\n @Log('Receiving a GET /route2 request...')\n route2() {\n return new HttpResponseOK('Hello world!');\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"forward-data-between-hooks",children:"Forward Data Between Hooks"}),"\n",(0,t.jsxs)(n.p,{children:["If you need to transfer data from one hook to another or to the controller method, you can use the ",(0,t.jsx)(n.code,{children:"state"})," property of the ",(0,t.jsx)(n.code,{children:"Context"})," object to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, Hook, HttpResponseOK, UserRequired } from '@foal/core';\nimport { Org } from '../entities';\n\nfunction AddOrgToContext() {\n return Hook(async ctx => {\n if (ctx.user) {\n ctx.state.org = await Org.findOneByOrFail({ id: ctx.user.orgId });\n }\n })\n}\n\nexport class ApiController {\n\n @Get('/org-name')\n @UserRequired()\n @AddOrgToContext()\n readOrgName(ctx: Context) {\n return new HttpResponseOK(ctx.state.org.name);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"If needed, you can also define an interface for your state and pass it as type argument to the context."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"interface State {\n org: Org;\n}\n\nexport class ApiController {\n // ...\n readOrgName(ctx: Context) {\n // ...\n }\n}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},28453:(e,n,o)=>{o.d(n,{R:()=>i,x:()=>c});var t=o(96540);const s={},r=t.createContext(s);function i(e){const n=t.useContext(r);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/17de4ea4.c50759b4.js b/assets/js/17de4ea4.028b96fd.js similarity index 73% rename from assets/js/17de4ea4.c50759b4.js rename to assets/js/17de4ea4.028b96fd.js index d64e4d3508..ceae9befd8 100644 --- a/assets/js/17de4ea4.c50759b4.js +++ b/assets/js/17de4ea4.028b96fd.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3428],{48178:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>i,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>d,toc:()=>l});var o=n(74848),r=n(28453);const a={title:"The Frontend App",id:"tuto-7-add-frontend",slug:"7-add-frontend"},s=void 0,d={id:"tutorials/real-world-example-with-react/tuto-7-add-frontend",title:"The Frontend App",description:"Very good, so far you have a first working version of your API. It's time to add the frontend.",source:"@site/docs/tutorials/real-world-example-with-react/7-add-frontend.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/7-add-frontend",permalink:"/docs/tutorials/real-world-example-with-react/7-add-frontend",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/7-add-frontend.md",tags:[],version:"current",sidebarPosition:7,frontMatter:{title:"The Frontend App",id:"tuto-7-add-frontend",slug:"7-add-frontend"},sidebar:"someSidebar",previous:{title:"API Testing with Swagger",permalink:"/docs/tutorials/real-world-example-with-react/6-swagger-interface"},next:{title:"Logging Users In and Out",permalink:"/docs/tutorials/real-world-example-with-react/8-authentication"}},i={},l=[];function c(e){const t={a:"a",code:"code",em:"em",img:"img",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"Very good, so far you have a first working version of your API. It's time to add the frontend."}),"\n",(0,o.jsxs)(t.p,{children:["Download the zip file ",(0,o.jsx)(t.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:n(71950).A+"",children:"here"}),". It contains a front-end code base that you will complete as you go along. Most of the application is already implemented for you. You will only have to deal with authentication and file uploads during this tutorial."]}),"\n",(0,o.jsxs)(t.p,{children:["Create a new directory ",(0,o.jsx)(t.code,{children:"frontend-app"})," at the root of your project and move the contents of the zip into it."]}),"\n",(0,o.jsx)(t.p,{children:"Go to the newly created directory and start the development server."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"cd frontend-app\nnpm install\nnpm run start\n"})}),"\n",(0,o.jsxs)(t.p,{children:["The frontend application loads at ",(0,o.jsx)(t.a,{href:"http://localhost:3000",children:"http://localhost:3000"}),"."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Feed page",src:n(45698).A+"",width:"2560",height:"1392"})}),"\n",(0,o.jsx)(t.p,{children:"The interface displays an error and prompts you to refresh the page. This error is due to the fact that the frontend and backend applications are served on different ports. So when sending a request, the frontend sends it to the wrong port."}),"\n",(0,o.jsxs)(t.p,{children:["One way to solve this problem is to temporarily update the ",(0,o.jsx)(t.code,{children:"requests/stories.ts"})," file to use the port ",(0,o.jsx)(t.code,{children:"3001"})," in development. But this forces you to add different code than is actually used in production, and it also generates ",(0,o.jsx)(t.em,{children:"same-origin policy"})," errors that you will still have to deal with."]}),"\n",(0,o.jsxs)(t.p,{children:["Another way to solve this problem is to ",(0,o.jsx)(t.em,{children:"connect"})," your front-end development server to port 3001 in development. This can be done with the following command."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"cd ../backend-app\nfoal connect react ../frontend-app\n"})}),"\n",(0,o.jsxs)(t.p,{children:["If you restart the frontend server, the stories should display correctly on the ",(0,o.jsx)(t.em,{children:"feed"})," page (except for the images)."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Feed page",src:n(66807).A+"",width:"2556",height:"1394"})})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(c,{...e})}):c(e)}},71950:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/files/frontend-app-e8a9536b1653a6e928a0048d0de7ec0d.zip"},45698:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/images/feed-error-8554029aeece9fa1bcc5f6bb0aeba30d.png"},66807:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/images/feed-no-images-8fea5ef99aff64389b1318caef2a3937.png"},28453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>d});var o=n(96540);const r={},a=o.createContext(r);function s(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function d(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3428],{48178:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>i,contentTitle:()=>s,default:()=>p,frontMatter:()=>a,metadata:()=>d,toc:()=>l});var o=n(74848),r=n(28453);const a={title:"The Frontend App",id:"tuto-7-add-frontend",slug:"7-add-frontend"},s=void 0,d={id:"tutorials/real-world-example-with-react/tuto-7-add-frontend",title:"The Frontend App",description:"Very good, so far you have a first working version of your API. It's time to add the frontend.",source:"@site/docs/tutorials/real-world-example-with-react/7-add-frontend.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/7-add-frontend",permalink:"/docs/tutorials/real-world-example-with-react/7-add-frontend",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/7-add-frontend.md",tags:[],version:"current",sidebarPosition:7,frontMatter:{title:"The Frontend App",id:"tuto-7-add-frontend",slug:"7-add-frontend"},sidebar:"someSidebar",previous:{title:"API Testing with Swagger",permalink:"/docs/tutorials/real-world-example-with-react/6-swagger-interface"},next:{title:"Logging Users In and Out",permalink:"/docs/tutorials/real-world-example-with-react/8-authentication"}},i={},l=[];function c(e){const t={a:"a",code:"code",em:"em",img:"img",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"Very good, so far you have a first working version of your API. It's time to add the frontend."}),"\n",(0,o.jsxs)(t.p,{children:["Download the zip file ",(0,o.jsx)(t.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:n(71950).A+"",children:"here"}),". It contains a front-end code base that you will complete as you go along. Most of the application is already implemented for you. You will only have to deal with authentication and file uploads during this tutorial."]}),"\n",(0,o.jsxs)(t.p,{children:["Create a new directory ",(0,o.jsx)(t.code,{children:"frontend-app"})," at the root of your project and move the contents of the zip into it."]}),"\n",(0,o.jsx)(t.p,{children:"Go to the newly created directory and start the development server."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"cd frontend-app\nnpm install\nnpm run start\n"})}),"\n",(0,o.jsxs)(t.p,{children:["The frontend application loads at ",(0,o.jsx)(t.a,{href:"http://localhost:3000",children:"http://localhost:3000"}),"."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Feed page",src:n(45698).A+"",width:"2560",height:"1392"})}),"\n",(0,o.jsx)(t.p,{children:"The interface displays an error and prompts you to refresh the page. This error is due to the fact that the frontend and backend applications are served on different ports. So when sending a request, the frontend sends it to the wrong port."}),"\n",(0,o.jsxs)(t.p,{children:["One way to solve this problem is to temporarily update the ",(0,o.jsx)(t.code,{children:"requests/stories.ts"})," file to use the port ",(0,o.jsx)(t.code,{children:"3001"})," in development. But this forces you to add different code than is actually used in production, and it also generates ",(0,o.jsx)(t.em,{children:"same-origin policy"})," errors that you will still have to deal with."]}),"\n",(0,o.jsxs)(t.p,{children:["Another way to solve this problem is to ",(0,o.jsx)(t.em,{children:"connect"})," your front-end development server to port 3001 in development. This can be done with the following command."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"cd ../backend-app\nnpx foal connect react ../frontend-app\n"})}),"\n",(0,o.jsxs)(t.p,{children:["If you restart the frontend server, the stories should display correctly on the ",(0,o.jsx)(t.em,{children:"feed"})," page (except for the images)."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Feed page",src:n(66807).A+"",width:"2556",height:"1394"})})]})}function p(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(c,{...e})}):c(e)}},71950:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/files/frontend-app-e8a9536b1653a6e928a0048d0de7ec0d.zip"},45698:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/images/feed-error-8554029aeece9fa1bcc5f6bb0aeba30d.png"},66807:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/images/feed-no-images-8fea5ef99aff64389b1318caef2a3937.png"},28453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>d});var o=n(96540);const r={},a=o.createContext(r);function s(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function d(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1877539e.14cbf4a2.js b/assets/js/1877539e.14cbf4a2.js deleted file mode 100644 index 348a70f60b..0000000000 --- a/assets/js/1877539e.14cbf4a2.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1172],{86127:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>a,contentTitle:()=>l,default:()=>p,frontMatter:()=>t,metadata:()=>i,toc:()=>c});var o=n(74848),s=n(28453);const t={title:"GraphQL"},l=void 0,i={id:"common/graphql",title:"GraphQL",description:"GraphQL is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for.",source:"@site/docs/common/graphql.md",sourceDirName:"common",slug:"/common/graphql",permalink:"/docs/common/graphql",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/graphql.md",tags:[],version:"current",frontMatter:{title:"GraphQL"},sidebar:"someSidebar",previous:{title:"OpenAPI",permalink:"/docs/common/openapi-and-swagger-ui"},next:{title:"WebSockets",permalink:"/docs/common/websockets"}},a={},c=[{value:"Basic Usage",id:"basic-usage",level:2},{value:"Using Separate Files for Type Definitions",id:"using-separate-files-for-type-definitions",level:2},{value:"Using a Service for the Root Resolvers",id:"using-a-service-for-the-root-resolvers",level:2},{value:"GraphiQL",id:"graphiql",level:2},{value:"Custom GraphiQL Options",id:"custom-graphiql-options",level:3},{value:"Custom API endpoint",id:"custom-api-endpoint",level:3},{value:"Custom CSS theme",id:"custom-css-theme",level:3},{value:"Error Handling - Masking & Logging Errors",id:"error-handling---masking--logging-errors",level:2},{value:"Authentication & Authorization",id:"authentication--authorization",level:2},{value:"Using TypeGraphQL",id:"using-typegraphql",level:2},{value:"Advanced",id:"advanced",level:2},{value:"Override the Resolver Context",id:"override-the-resolver-context",level:3}];function h(e){const r={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(r.p,{children:[(0,o.jsx)(r.a,{href:"https://graphql.org/",children:"GraphQL"})," is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example of request"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-graphql",children:'{\n project(name: "GraphQL") {\n tagline\n }\n}\n'})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example of response"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-json",children:'{\n "data": {\n "project": {\n "tagline": "A query language for APIs"\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsx)(r.p,{children:"The below document assumes that you have a basic knowledge of GraphQL."}),"\n"]}),"\n",(0,o.jsxs)(r.p,{children:["To use GraphQL with FoalTS, you need to install the packages ",(0,o.jsx)(r.code,{children:"graphql"})," and ",(0,o.jsx)(r.code,{children:"@foal/graphql"}),". The first one is maintained by the GraphQL community and parses and resolves queries. The second is specific to FoalTS and allows you to configure a controller compatible with common GraphQL clients (",(0,o.jsx)(r.a,{href:"https://www.npmjs.com/package/graphql-request",children:"graphql-request"}),", etc), load type definitions from separate files or handle errors thrown in resolvers."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-bash",children:"npm install graphql@16 @foal/graphql\n"})}),"\n",(0,o.jsx)(r.h2,{id:"basic-usage",children:"Basic Usage"}),"\n",(0,o.jsxs)(r.p,{children:["The main component of the package is the abstract ",(0,o.jsx)(r.code,{children:"GraphQLController"}),". Inheriting this class allows you to create a controller that is compatible with common GraphQL clients (",(0,o.jsx)(r.a,{href:"https://www.npmjs.com/package/graphql-request",children:"graphql-request"}),", etc) or any client that follows the HTTP specification defined ",(0,o.jsx)(r.a,{href:"https://graphql.org/learn/serving-over-http/",children:"here"}),"."]}),"\n",(0,o.jsx)(r.p,{children:"Here is an example on how to use it with a simple schema and resolver."}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"app.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"export class AppController {\n subControllers = [\n controller('/graphql', ApiController)\n ]\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController } from '@foal/graphql';\nimport { buildSchema } from 'graphql';\n\nconst schema = buildSchema(`\n type Query {\n hello: String\n }\n`);\n\nconst root = {\n hello: () => {\n return 'Hello world!';\n },\n};\n\nexport class ApiController extends GraphQLController {\n schema = schema;\n resolvers = root;\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:"And here is an example of what your client code might look like:"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { request } from 'graphql-request';\n\nconst data = await request('/graphql', '{ hello }');\n// data equals \"{ hello: 'Hello world!' }\"\n"})}),"\n",(0,o.jsxs)(r.p,{children:["Alternatively, if you have several strings that define your GraphQL types, you can use the ",(0,o.jsx)(r.code,{children:"schemaFromTypeDefs"})," function to build the schema."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController, schemaFromTypeDefs } from '@foal/graphql';\n\nconst source1 = `\n type Query {\n me: User\n }\n`;\nconst source2 = `\n type User {\n id: ID\n name: String\n }\n`;\n\n// ...\n\nexport class ApiController extends GraphQLController {\n schema = schemaFromTypeDefs(source1, source2);\n // ...\n}\n\n"})}),"\n",(0,o.jsx)(r.h2,{id:"using-separate-files-for-type-definitions",children:"Using Separate Files for Type Definitions"}),"\n",(0,o.jsxs)(r.p,{children:["If you want to specify type definitions in separate files, you can use the ",(0,o.jsx)(r.code,{children:"schemaFromTypeGlob"})," function for this."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{children:"src/\n'- app/\n '- controllers/\n |- query.graphql\n |- user.graphql\n '- api.controller.ts\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"query.graphql"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-graphql",children:"type Query {\n me: User\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"user.graphql"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-graphql",children:"type User {\n id: ID\n name: String\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController, schemaFromTypeGlob } from '@foal/graphql';\nimport { join } from 'path';\n\nexport class ApiController extends GraphQLController {\n schema = schemaFromTypeGlob(join(__dirname, '**/*.graphql'));\n // ...\n}\n"})}),"\n",(0,o.jsxs)(r.p,{children:["Note that for this to work, you must copy the graphql files during the build. To do this, you need to install the ",(0,o.jsx)(r.code,{children:"copy"})," package and update some commands of your ",(0,o.jsx)(r.code,{children:"package.json"}),"."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{children:"npm install cpx2 --save-dev\n"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-json",children:'{\n "scripts": {\n "build": "foal rmdir build && cpx \\"src/**/*.graphql\\" build && tsc -p tsconfig.app.json",\n "dev": "npm run build && concurrently \\"cpx \\\\\\"src/**/*.graphql\\\\\\" build -w\\" \\"tsc -p tsconfig.app.json -w\\" \\"supervisor -w ./build,./config -e js,json,yml,graphql --no-restart-on error ./build/index.js\\"",\n "build:test": "foal rmdir build && cpx \\"src/**/*.graphql\\" build && tsc -p tsconfig.test.json",\n "test": "npm run build:test && concurrently \\"cpx \\\\\\"src/**/*.graphql\\\\\\" build -w\\" \\"tsc -p tsconfig.test.json -w\\" \\"mocha --file ./build/test.js -w --watch-files build \\\\\\"./build/**/*.spec.js\\\\\\"\\"",\n "build:e2e": "foal rmdir build && cpx \\"src/**/*.graphql\\" build && tsc -p tsconfig.e2e.json",\n "e2e": "npm run build:e2e && concurrently \\"cpx \\\\\\"src/**/*.graphql\\\\\\" build -w\\" \\"tsc -p tsconfig.e2e.json -w\\" \\"mocha --file ./build/e2e.js -w --watch-files build \\\\\\"./build/e2e/**/*.js\\\\\\"\\"",\n ...\n }\n}\n'})}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsxs)(r.p,{children:["Alternatively, if you want to specify only specific files instead of using a glob pattern, you can call ",(0,o.jsx)(r.code,{children:"schemaFromTypePaths"}),"."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController, schemaFromTypePaths } from '@foal/graphql';\nimport { join } from 'path';\n\nexport class ApiController extends GraphQLController {\n schema = schemaFromTypePaths(\n join(__dirname, './query.graphql'),\n join(__dirname, './user.graphql')\n );\n // ...\n}\n"})}),"\n"]}),"\n",(0,o.jsx)(r.h2,{id:"using-a-service-for-the-root-resolvers",children:"Using a Service for the Root Resolvers"}),"\n",(0,o.jsxs)(r.p,{children:["Root resolvers can also be grouped into a service in order to benefit from all the advantages offered by services (dependency injection, etc.). All you have to do is add the ",(0,o.jsx)(r.code,{children:"@dependency"})," decorator as you would with any service."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { dependency } from '@foal/core';\nimport { GraphQLController } from '@foal/graphql';\nimport { RootResolverService } from '../services';\n\n// ...\n\nexport class ApiController extends GraphQLController {\n schema = // ...\n\n @dependency\n resolvers: RootResolverService;\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"root-resolver.service.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"export class RootResolverService {\n\n hello() {\n return 'Hello world!';\n }\n\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"graphiql",children:"GraphiQL"}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.img,{alt:"GraphiQL",src:n(84661).A+"",width:"988",height:"601"})}),"\n",(0,o.jsxs)(r.p,{children:["You can generate a ",(0,o.jsx)(r.code,{children:"GraphiQL"})," page with the ",(0,o.jsx)(r.code,{children:"GraphiQLController"})," class by installing the following package."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-bash",children:"npm install @foal/graphiql\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"app.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { controller } from '@foal/core';\nimport { GraphiQLController } from '@foal/graphiql';\n\nimport { GraphqlApiController } from './services';\n\nexport class AppController {\n\n subControllers = [\n // ...\n controller('/graphql', GraphqlApiController),\n controller('/graphiql', GraphiQLController)\n ];\n\n}\n"})}),"\n",(0,o.jsx)(r.h3,{id:"custom-graphiql-options",children:"Custom GraphiQL Options"}),"\n",(0,o.jsxs)(r.p,{children:["Most ",(0,o.jsx)(r.a,{href:"https://github.com/graphql/graphiql/tree/main/packages/graphiql#props",children:"GraphiQL options"})," are supported and can be provided by inheriting the ",(0,o.jsx)(r.code,{children:"GraphiQLController"})," class."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphiQLController, GraphiQLControllerOptions } from '@foal/graphiql';\n\nexport class GraphiQL2Controller extends GraphiQLController {\n options: GraphiQLControllerOptions = {\n docExplorerOpen: true,\n }\n}\n\n"})}),"\n",(0,o.jsx)(r.h3,{id:"custom-api-endpoint",children:"Custom API endpoint"}),"\n",(0,o.jsxs)(r.p,{children:["By default, the GraphiQL page assumes that the GraphiQL API is located at ",(0,o.jsx)(r.code,{children:"/graphql"}),". This behavior can be overridden with the ",(0,o.jsx)(r.code,{children:"apiEndpoint"})," property."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphiQLController, GraphiQLControllerOptions } from '@foal/graphiql';\n\nexport class GraphiQL2Controller extends GraphiQLController {\n apiEndpoint = '/api';\n}\n\n"})}),"\n",(0,o.jsx)(r.h3,{id:"custom-css-theme",children:"Custom CSS theme"}),"\n",(0,o.jsx)(r.p,{children:"In order to change the page theme, the controller class allows you to include custom CSS files."}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphiQLController, GraphiQLControllerOptions } from '@foal/graphiql';\n\nexport class GraphiQL2Controller extends GraphiQLController {\n cssThemeURL = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/theme/solarized.css';\n\n options: GraphiQLControllerOptions = {\n editorTheme: 'solarized light'\n }\n}\n\n"})}),"\n",(0,o.jsx)(r.h2,{id:"error-handling---masking--logging-errors",children:"Error Handling - Masking & Logging Errors"}),"\n",(0,o.jsx)(r.p,{children:"By default, GraphQL returns all errors thrown (or rejected) in the resolvers. However, this behavior is often not desired in production as it could cause sensitive information to leak from the server."}),"\n",(0,o.jsxs)(r.p,{children:["In comparison with REST APIs, when the ",(0,o.jsx)(r.a,{href:"/docs/architecture/configuration",children:"configuration key"})," ",(0,o.jsx)(r.code,{children:"settings.debug"})," does not equal ",(0,o.jsx)(r.code,{children:"true"})," (production case), details of the errors thrown in controllers are not returned to the client. Only a ",(0,o.jsx)(r.code,{children:"500 - Internal Server Error"})," error is sent back."]}),"\n",(0,o.jsxs)(r.p,{children:["In a similar way, FoalTS provides two utilities ",(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"})," for your GraphQL APIs to log and mask errors. When ",(0,o.jsx)(r.code,{children:"settings.debug"})," is ",(0,o.jsx)(r.code,{children:"true"}),", the errors are converted into a new one whose unique message is ",(0,o.jsx)(r.code,{children:"Internal Server Error"}),"."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example of GraphQL response in production"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-json",children:'{\n "data": { "user": null },\n "errors": [\n {\n "locations": [ { "column": 2, "line": 1 } ],\n "message": "Internal Server Error",\n "path": [ "user" ]\n }\n ]\n}\n'})}),"\n",(0,o.jsxs)(r.p,{children:["Here are examples on how to use ",(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"}),"."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"function user() {\n // ...\n}\n\nconst resolvers = {\n user: formatError(user)\n}\n"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"class ResolverService {\n @FormatError()\n user() {\n // ...\n }\n}\n"})}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsxs)(r.p,{children:["Note that ",(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"})," make your functions become asynchronous. This means that any value returned by the function is now a resolved promise of this value, and any errors thrown in the function is converted into a rejected promise. This only has an impact on unit testing as you may need to preceed your function calls by the keyword ",(0,o.jsx)(r.code,{children:"await"}),"."]}),"\n"]}),"\n",(0,o.jsxs)(r.p,{children:[(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"})," also accept an optional parameter to override its default behavior. It is a function that takes the error thrown or rejected in the resolver and return the error that must be sent to the client. It may be asynchronous or synchronous."]}),"\n",(0,o.jsx)(r.p,{children:"By default, this function is:"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"function maskAndLogError(err: any): any {\n console.log(err);\n\n if (Config.get('settings.debug', 'boolean')) {\n return err;\n }\n\n return new Error('Internal Server Error');\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:"But we can also imagine other implementations such as:"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { reportErrorTo3rdPartyService } from 'somewhere';\n\nasync function maskAndLogError(err: any): Promise {\n console.log(err);\n\n try {\n await reportErrorTo3rdPartyService(err);\n } catch (error: any) {}\n\n if (err instanceof MyCustomError) {\n return err;\n }\n\n if (Config.get('settings.debug', 'boolean')) {\n return err;\n }\n\n return new Error('Internal Server Error');\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"authentication--authorization",children:"Authentication & Authorization"}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsx)(r.p,{children:"The below code is an example of managing authentication and authorization with a GraphQL controller."}),"\n"]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController } from '@foal/graphql';\nimport { JWTRequired } from '@foal/jwt';\nimport { buildSchema } from 'graphql';\n\nimport { User } from '../entities';\n\nconst schema = buildSchema(`\n type Query {\n hello: String\n }\n`);\n\nconst root = {\n hello: (_, context) => {\n if (!context.user.isAdmin) {\n return null;\n }\n return 'Hello world!';\n },\n};\n\n@JWTRequired({ user: (id: number) => User.findOneBy({ id }) })\nexport class ApiController extends GraphQLController {\n schema = schema;\n resolvers = root;\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"using-typegraphql",children:"Using TypeGraphQL"}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsx)(r.p,{children:(0,o.jsxs)(r.em,{children:[(0,o.jsx)(r.a,{href:"https://typegraphql.com/",children:"TypeGraphQL"})," is a library that allows you to create GraphQL schemas and resolvers with TypeScript classes and decorators."]})}),"\n"]}),"\n",(0,o.jsxs)(r.p,{children:["You can use TypeGraphQL by simply calling its ",(0,o.jsx)(r.code,{children:"buildSchema"})," function."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController } from '@foal/graphql';\nimport { buildSchema, Field, ObjectType, Query, Resolver } from 'type-graphql';\n\n@ObjectType()\nclass Recipe {\n @Field()\n title: string;\n}\n\n@Resolver(Recipe)\nclass RecipeResolver {\n\n @Query(returns => Recipe)\n async recipe() {\n return {\n title: 'foobar'\n };\n }\n\n}\n\nexport class ApiController extends GraphQLController {\n schema = buildSchema({\n resolvers: [ RecipeResolver ]\n });\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"advanced",children:"Advanced"}),"\n",(0,o.jsx)(r.h3,{id:"override-the-resolver-context",children:"Override the Resolver Context"}),"\n",(0,o.jsxs)(r.p,{children:["The ",(0,o.jsx)(r.em,{children:"GraphQL context"})," that is passed to the resolvers is by default the ",(0,o.jsx)(r.em,{children:"request context"}),". This behavior can be changed by overriding the ",(0,o.jsx)(r.code,{children:"getResolverContext"})," method."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { Context } from '@foal/core';\nimport { GraphQLController } from '@foal/graphql';\n\nexport class ApiController extends GraphQLController {\n // ...\n\n getResolverContext(ctx: Context) {\n return { user: ctx.user };\n }\n}\n"})})]})}function p(e={}){const{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,o.jsx)(r,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},84661:(e,r,n)=>{n.d(r,{A:()=>o});const o=n.p+"assets/images/graphiql-f099ed2191f41190bd6569d1b5cd1267.png"},28453:(e,r,n)=>{n.d(r,{R:()=>l,x:()=>i});var o=n(96540);const s={},t=o.createContext(s);function l(e){const r=o.useContext(t);return o.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function i(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:l(e.components),o.createElement(t.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1877539e.b70c1a05.js b/assets/js/1877539e.b70c1a05.js new file mode 100644 index 0000000000..0d6ce02795 --- /dev/null +++ b/assets/js/1877539e.b70c1a05.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1172],{86127:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>a,contentTitle:()=>l,default:()=>p,frontMatter:()=>t,metadata:()=>i,toc:()=>c});var o=n(74848),s=n(28453);const t={title:"GraphQL"},l=void 0,i={id:"common/graphql",title:"GraphQL",description:"GraphQL is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for.",source:"@site/docs/common/graphql.md",sourceDirName:"common",slug:"/common/graphql",permalink:"/docs/common/graphql",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/graphql.md",tags:[],version:"current",frontMatter:{title:"GraphQL"},sidebar:"someSidebar",previous:{title:"OpenAPI",permalink:"/docs/common/openapi-and-swagger-ui"},next:{title:"WebSockets",permalink:"/docs/common/websockets"}},a={},c=[{value:"Basic Usage",id:"basic-usage",level:2},{value:"Using Separate Files for Type Definitions",id:"using-separate-files-for-type-definitions",level:2},{value:"Using a Service for the Root Resolvers",id:"using-a-service-for-the-root-resolvers",level:2},{value:"GraphiQL",id:"graphiql",level:2},{value:"Custom GraphiQL Options",id:"custom-graphiql-options",level:3},{value:"Custom API endpoint",id:"custom-api-endpoint",level:3},{value:"Custom CSS theme",id:"custom-css-theme",level:3},{value:"Error Handling - Masking & Logging Errors",id:"error-handling---masking--logging-errors",level:2},{value:"Authentication & Authorization",id:"authentication--authorization",level:2},{value:"Using TypeGraphQL",id:"using-typegraphql",level:2},{value:"Advanced",id:"advanced",level:2},{value:"Override the Resolver Context",id:"override-the-resolver-context",level:3}];function h(e){const r={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(r.p,{children:[(0,o.jsx)(r.a,{href:"https://graphql.org/",children:"GraphQL"})," is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example of request"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-graphql",children:'{\n project(name: "GraphQL") {\n tagline\n }\n}\n'})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example of response"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-json",children:'{\n "data": {\n "project": {\n "tagline": "A query language for APIs"\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsx)(r.p,{children:"The below document assumes that you have a basic knowledge of GraphQL."}),"\n"]}),"\n",(0,o.jsxs)(r.p,{children:["To use GraphQL with FoalTS, you need to install the packages ",(0,o.jsx)(r.code,{children:"graphql"})," and ",(0,o.jsx)(r.code,{children:"@foal/graphql"}),". The first one is maintained by the GraphQL community and parses and resolves queries. The second is specific to FoalTS and allows you to configure a controller compatible with common GraphQL clients (",(0,o.jsx)(r.a,{href:"https://www.npmjs.com/package/graphql-request",children:"graphql-request"}),", etc), load type definitions from separate files or handle errors thrown in resolvers."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-bash",children:"npm install graphql@16 @foal/graphql\n"})}),"\n",(0,o.jsx)(r.h2,{id:"basic-usage",children:"Basic Usage"}),"\n",(0,o.jsxs)(r.p,{children:["The main component of the package is the abstract ",(0,o.jsx)(r.code,{children:"GraphQLController"}),". Inheriting this class allows you to create a controller that is compatible with common GraphQL clients (",(0,o.jsx)(r.a,{href:"https://www.npmjs.com/package/graphql-request",children:"graphql-request"}),", etc) or any client that follows the HTTP specification defined ",(0,o.jsx)(r.a,{href:"https://graphql.org/learn/serving-over-http/",children:"here"}),"."]}),"\n",(0,o.jsx)(r.p,{children:"Here is an example on how to use it with a simple schema and resolver."}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"app.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"export class AppController {\n subControllers = [\n controller('/graphql', ApiController)\n ]\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController } from '@foal/graphql';\nimport { buildSchema } from 'graphql';\n\nconst schema = buildSchema(`\n type Query {\n hello: String\n }\n`);\n\nconst root = {\n hello: () => {\n return 'Hello world!';\n },\n};\n\nexport class ApiController extends GraphQLController {\n schema = schema;\n resolvers = root;\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:"And here is an example of what your client code might look like:"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { request } from 'graphql-request';\n\nconst data = await request('/graphql', '{ hello }');\n// data equals \"{ hello: 'Hello world!' }\"\n"})}),"\n",(0,o.jsxs)(r.p,{children:["Alternatively, if you have several strings that define your GraphQL types, you can use the ",(0,o.jsx)(r.code,{children:"schemaFromTypeDefs"})," function to build the schema."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController, schemaFromTypeDefs } from '@foal/graphql';\n\nconst source1 = `\n type Query {\n me: User\n }\n`;\nconst source2 = `\n type User {\n id: ID\n name: String\n }\n`;\n\n// ...\n\nexport class ApiController extends GraphQLController {\n schema = schemaFromTypeDefs(source1, source2);\n // ...\n}\n\n"})}),"\n",(0,o.jsx)(r.h2,{id:"using-separate-files-for-type-definitions",children:"Using Separate Files for Type Definitions"}),"\n",(0,o.jsxs)(r.p,{children:["If you want to specify type definitions in separate files, you can use the ",(0,o.jsx)(r.code,{children:"schemaFromTypeGlob"})," function for this."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{children:"src/\n'- app/\n '- controllers/\n |- query.graphql\n |- user.graphql\n '- api.controller.ts\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"query.graphql"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-graphql",children:"type Query {\n me: User\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"user.graphql"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-graphql",children:"type User {\n id: ID\n name: String\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController, schemaFromTypeGlob } from '@foal/graphql';\nimport { join } from 'path';\n\nexport class ApiController extends GraphQLController {\n schema = schemaFromTypeGlob(join(__dirname, '**/*.graphql'));\n // ...\n}\n"})}),"\n",(0,o.jsxs)(r.p,{children:["Note that for this to work, you must copy the graphql files during the build. To do this, you need to install the ",(0,o.jsx)(r.code,{children:"copy"})," package and update some commands of your ",(0,o.jsx)(r.code,{children:"package.json"}),"."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{children:"npm install cpx2 --save-dev\n"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-json",children:'{\n "scripts": {\n "build": "npx foal rmdir build && cpx \\"src/**/*.graphql\\" build && tsc -p tsconfig.app.json",\n "dev": "npm run build && concurrently \\"cpx \\\\\\"src/**/*.graphql\\\\\\" build -w\\" \\"tsc -p tsconfig.app.json -w\\" \\"supervisor -w ./build,./config -e js,json,yml,graphql --no-restart-on error ./build/index.js\\"",\n "build:test": "npx foal rmdir build && cpx \\"src/**/*.graphql\\" build && tsc -p tsconfig.test.json",\n "test": "npm run build:test && concurrently \\"cpx \\\\\\"src/**/*.graphql\\\\\\" build -w\\" \\"tsc -p tsconfig.test.json -w\\" \\"mocha --file ./build/test.js -w --watch-files build \\\\\\"./build/**/*.spec.js\\\\\\"\\"",\n "build:e2e": "npx foal rmdir build && cpx \\"src/**/*.graphql\\" build && tsc -p tsconfig.e2e.json",\n "e2e": "npm run build:e2e && concurrently \\"cpx \\\\\\"src/**/*.graphql\\\\\\" build -w\\" \\"tsc -p tsconfig.e2e.json -w\\" \\"mocha --file ./build/e2e.js -w --watch-files build \\\\\\"./build/e2e/**/*.js\\\\\\"\\"",\n ...\n }\n}\n'})}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsxs)(r.p,{children:["Alternatively, if you want to specify only specific files instead of using a glob pattern, you can call ",(0,o.jsx)(r.code,{children:"schemaFromTypePaths"}),"."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController, schemaFromTypePaths } from '@foal/graphql';\nimport { join } from 'path';\n\nexport class ApiController extends GraphQLController {\n schema = schemaFromTypePaths(\n join(__dirname, './query.graphql'),\n join(__dirname, './user.graphql')\n );\n // ...\n}\n"})}),"\n"]}),"\n",(0,o.jsx)(r.h2,{id:"using-a-service-for-the-root-resolvers",children:"Using a Service for the Root Resolvers"}),"\n",(0,o.jsxs)(r.p,{children:["Root resolvers can also be grouped into a service in order to benefit from all the advantages offered by services (dependency injection, etc.). All you have to do is add the ",(0,o.jsx)(r.code,{children:"@dependency"})," decorator as you would with any service."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { dependency } from '@foal/core';\nimport { GraphQLController } from '@foal/graphql';\nimport { RootResolverService } from '../services';\n\n// ...\n\nexport class ApiController extends GraphQLController {\n schema = // ...\n\n @dependency\n resolvers: RootResolverService;\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"root-resolver.service.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"export class RootResolverService {\n\n hello() {\n return 'Hello world!';\n }\n\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"graphiql",children:"GraphiQL"}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.img,{alt:"GraphiQL",src:n(84661).A+"",width:"988",height:"601"})}),"\n",(0,o.jsxs)(r.p,{children:["You can generate a ",(0,o.jsx)(r.code,{children:"GraphiQL"})," page with the ",(0,o.jsx)(r.code,{children:"GraphiQLController"})," class by installing the following package."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-bash",children:"npm install @foal/graphiql\n"})}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"app.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { controller } from '@foal/core';\nimport { GraphiQLController } from '@foal/graphiql';\n\nimport { GraphqlApiController } from './services';\n\nexport class AppController {\n\n subControllers = [\n // ...\n controller('/graphql', GraphqlApiController),\n controller('/graphiql', GraphiQLController)\n ];\n\n}\n"})}),"\n",(0,o.jsx)(r.h3,{id:"custom-graphiql-options",children:"Custom GraphiQL Options"}),"\n",(0,o.jsxs)(r.p,{children:["Most ",(0,o.jsx)(r.a,{href:"https://github.com/graphql/graphiql/tree/main/packages/graphiql#props",children:"GraphiQL options"})," are supported and can be provided by inheriting the ",(0,o.jsx)(r.code,{children:"GraphiQLController"})," class."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphiQLController, GraphiQLControllerOptions } from '@foal/graphiql';\n\nexport class GraphiQL2Controller extends GraphiQLController {\n options: GraphiQLControllerOptions = {\n docExplorerOpen: true,\n }\n}\n\n"})}),"\n",(0,o.jsx)(r.h3,{id:"custom-api-endpoint",children:"Custom API endpoint"}),"\n",(0,o.jsxs)(r.p,{children:["By default, the GraphiQL page assumes that the GraphiQL API is located at ",(0,o.jsx)(r.code,{children:"/graphql"}),". This behavior can be overridden with the ",(0,o.jsx)(r.code,{children:"apiEndpoint"})," property."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphiQLController, GraphiQLControllerOptions } from '@foal/graphiql';\n\nexport class GraphiQL2Controller extends GraphiQLController {\n apiEndpoint = '/api';\n}\n\n"})}),"\n",(0,o.jsx)(r.h3,{id:"custom-css-theme",children:"Custom CSS theme"}),"\n",(0,o.jsx)(r.p,{children:"In order to change the page theme, the controller class allows you to include custom CSS files."}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphiQLController, GraphiQLControllerOptions } from '@foal/graphiql';\n\nexport class GraphiQL2Controller extends GraphiQLController {\n cssThemeURL = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/theme/solarized.css';\n\n options: GraphiQLControllerOptions = {\n editorTheme: 'solarized light'\n }\n}\n\n"})}),"\n",(0,o.jsx)(r.h2,{id:"error-handling---masking--logging-errors",children:"Error Handling - Masking & Logging Errors"}),"\n",(0,o.jsx)(r.p,{children:"By default, GraphQL returns all errors thrown (or rejected) in the resolvers. However, this behavior is often not desired in production as it could cause sensitive information to leak from the server."}),"\n",(0,o.jsxs)(r.p,{children:["In comparison with REST APIs, when the ",(0,o.jsx)(r.a,{href:"/docs/architecture/configuration",children:"configuration key"})," ",(0,o.jsx)(r.code,{children:"settings.debug"})," does not equal ",(0,o.jsx)(r.code,{children:"true"})," (production case), details of the errors thrown in controllers are not returned to the client. Only a ",(0,o.jsx)(r.code,{children:"500 - Internal Server Error"})," error is sent back."]}),"\n",(0,o.jsxs)(r.p,{children:["In a similar way, FoalTS provides two utilities ",(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"})," for your GraphQL APIs to log and mask errors. When ",(0,o.jsx)(r.code,{children:"settings.debug"})," is ",(0,o.jsx)(r.code,{children:"true"}),", the errors are converted into a new one whose unique message is ",(0,o.jsx)(r.code,{children:"Internal Server Error"}),"."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example of GraphQL response in production"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-json",children:'{\n "data": { "user": null },\n "errors": [\n {\n "locations": [ { "column": 2, "line": 1 } ],\n "message": "Internal Server Error",\n "path": [ "user" ]\n }\n ]\n}\n'})}),"\n",(0,o.jsxs)(r.p,{children:["Here are examples on how to use ",(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"}),"."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"function user() {\n // ...\n}\n\nconst resolvers = {\n user: formatError(user)\n}\n"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"class ResolverService {\n @FormatError()\n user() {\n // ...\n }\n}\n"})}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsxs)(r.p,{children:["Note that ",(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"})," make your functions become asynchronous. This means that any value returned by the function is now a resolved promise of this value, and any errors thrown in the function is converted into a rejected promise. This only has an impact on unit testing as you may need to preceed your function calls by the keyword ",(0,o.jsx)(r.code,{children:"await"}),"."]}),"\n"]}),"\n",(0,o.jsxs)(r.p,{children:[(0,o.jsx)(r.code,{children:"formatError"})," and ",(0,o.jsx)(r.code,{children:"@FormatError"})," also accept an optional parameter to override its default behavior. It is a function that takes the error thrown or rejected in the resolver and return the error that must be sent to the client. It may be asynchronous or synchronous."]}),"\n",(0,o.jsx)(r.p,{children:"By default, this function is:"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"function maskAndLogError(err: any): any {\n console.log(err);\n\n if (Config.get('settings.debug', 'boolean')) {\n return err;\n }\n\n return new Error('Internal Server Error');\n}\n"})}),"\n",(0,o.jsx)(r.p,{children:"But we can also imagine other implementations such as:"}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { reportErrorTo3rdPartyService } from 'somewhere';\n\nasync function maskAndLogError(err: any): Promise {\n console.log(err);\n\n try {\n await reportErrorTo3rdPartyService(err);\n } catch (error: any) {}\n\n if (err instanceof MyCustomError) {\n return err;\n }\n\n if (Config.get('settings.debug', 'boolean')) {\n return err;\n }\n\n return new Error('Internal Server Error');\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"authentication--authorization",children:"Authentication & Authorization"}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsx)(r.p,{children:"The below code is an example of managing authentication and authorization with a GraphQL controller."}),"\n"]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"api.controller.ts"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController } from '@foal/graphql';\nimport { JWTRequired } from '@foal/jwt';\nimport { buildSchema } from 'graphql';\n\nimport { User } from '../entities';\n\nconst schema = buildSchema(`\n type Query {\n hello: String\n }\n`);\n\nconst root = {\n hello: (_, context) => {\n if (!context.user.isAdmin) {\n return null;\n }\n return 'Hello world!';\n },\n};\n\n@JWTRequired({ user: (id: number) => User.findOneBy({ id }) })\nexport class ApiController extends GraphQLController {\n schema = schema;\n resolvers = root;\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"using-typegraphql",children:"Using TypeGraphQL"}),"\n",(0,o.jsxs)(r.blockquote,{children:["\n",(0,o.jsx)(r.p,{children:(0,o.jsxs)(r.em,{children:[(0,o.jsx)(r.a,{href:"https://typegraphql.com/",children:"TypeGraphQL"})," is a library that allows you to create GraphQL schemas and resolvers with TypeScript classes and decorators."]})}),"\n"]}),"\n",(0,o.jsxs)(r.p,{children:["You can use TypeGraphQL by simply calling its ",(0,o.jsx)(r.code,{children:"buildSchema"})," function."]}),"\n",(0,o.jsx)(r.p,{children:(0,o.jsx)(r.em,{children:"Example"})}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { GraphQLController } from '@foal/graphql';\nimport { buildSchema, Field, ObjectType, Query, Resolver } from 'type-graphql';\n\n@ObjectType()\nclass Recipe {\n @Field()\n title: string;\n}\n\n@Resolver(Recipe)\nclass RecipeResolver {\n\n @Query(returns => Recipe)\n async recipe() {\n return {\n title: 'foobar'\n };\n }\n\n}\n\nexport class ApiController extends GraphQLController {\n schema = buildSchema({\n resolvers: [ RecipeResolver ]\n });\n}\n"})}),"\n",(0,o.jsx)(r.h2,{id:"advanced",children:"Advanced"}),"\n",(0,o.jsx)(r.h3,{id:"override-the-resolver-context",children:"Override the Resolver Context"}),"\n",(0,o.jsxs)(r.p,{children:["The ",(0,o.jsx)(r.em,{children:"GraphQL context"})," that is passed to the resolvers is by default the ",(0,o.jsx)(r.em,{children:"request context"}),". This behavior can be changed by overriding the ",(0,o.jsx)(r.code,{children:"getResolverContext"})," method."]}),"\n",(0,o.jsx)(r.pre,{children:(0,o.jsx)(r.code,{className:"language-typescript",children:"import { Context } from '@foal/core';\nimport { GraphQLController } from '@foal/graphql';\n\nexport class ApiController extends GraphQLController {\n // ...\n\n getResolverContext(ctx: Context) {\n return { user: ctx.user };\n }\n}\n"})})]})}function p(e={}){const{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,o.jsx)(r,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},84661:(e,r,n)=>{n.d(r,{A:()=>o});const o=n.p+"assets/images/graphiql-f099ed2191f41190bd6569d1b5cd1267.png"},28453:(e,r,n)=>{n.d(r,{R:()=>l,x:()=>i});var o=n(96540);const s={},t=o.createContext(s);function l(e){const r=o.useContext(t);return o.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function i(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:l(e.components),o.createElement(t.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/23374ca6.345fbdcc.js b/assets/js/23374ca6.26b7b640.js similarity index 82% rename from assets/js/23374ca6.345fbdcc.js rename to assets/js/23374ca6.26b7b640.js index 0398081c39..d9ffe26b7a 100644 --- a/assets/js/23374ca6.345fbdcc.js +++ b/assets/js/23374ca6.26b7b640.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2278],{94842:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>r,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var i=o(74848),n=o(28453);const s={title:"Introduction",slug:"/"},a=void 0,l={id:"README",title:"Introduction",description:"License: MIT",source:"@site/docs/README.md",sourceDirName:".",slug:"/",permalink:"/docs/",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/README.md",tags:[],version:"current",frontMatter:{title:"Introduction",slug:"/"},sidebar:"someSidebar",next:{title:"Installation",permalink:"/docs/tutorials/simple-todo-list/1-installation"}},r={},c=[{value:"What is Foal?",id:"what-is-foal",level:2},{value:"Development Policy",id:"development-policy",level:2},{value:"Thousands of Tests",id:"thousands-of-tests",level:3},{value:"Documentation",id:"documentation",level:3},{value:"Product Stability",id:"product-stability",level:3},{value:"Get Started",id:"get-started",level:2}];function d(e){const t={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.img,{src:"https://img.shields.io/badge/License-MIT-blue.svg",alt:"License: MIT"}),"\n",(0,i.jsx)(t.img,{src:"https://snyk.io/test/github/foalts/foal/badge.svg",alt:"Known Vulnerabilities"}),"\n",(0,i.jsx)(t.img,{src:"https://img.shields.io/github/commit-activity/y/FoalTS/foal.svg",alt:"Commit activity"}),"\n",(0,i.jsx)(t.img,{src:"https://img.shields.io/github/last-commit/FoalTS/foal.svg",alt:"Last commit"})]}),"\n",(0,i.jsx)(t.h2,{id:"what-is-foal",children:"What is Foal?"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.em,{children:"Foal"})," (or ",(0,i.jsx)(t.em,{children:"FoalTS"}),") is a Node.JS framework for creating web applications."]}),"\n",(0,i.jsx)(t.p,{children:"It provides a set of ready-to-use components so you don't have to reinvent the wheel every time. In one single place, you have a complete environment to build web applications. This includes a CLI, testing tools, frontend utilities, scripts, advanced authentication, ORM, deployment environments, GraphQL and Swagger API, AWS utilities, and more. You no longer need to get lost on npm searching for packages and making them work together. All is provided."}),"\n",(0,i.jsx)(t.p,{children:"But while offering all these features, the framework remains simple. Complexity and unnecessary abstractions are put aside to provide the most intuitive and expressive syntax. We believe that concise and elegant code is the best way to develop an application and maintain it in the future. It also allows you to spend more time coding rather than trying to understand how the framework works."}),"\n",(0,i.jsx)(t.p,{children:"Finally, the framework is entirely written in TypeScript. The language brings you optional static type-checking along with the latest ECMAScript features. This allows you to detect most silly errors during compilation and improve the quality of your code. It also offers you autocompletion and a well documented API."}),"\n",(0,i.jsx)(t.h2,{id:"development-policy",children:"Development Policy"}),"\n",(0,i.jsx)(t.h3,{id:"thousands-of-tests",children:"Thousands of Tests"}),"\n",(0,i.jsx)(t.p,{children:"Testing FoalTS is put on a very high priority. Providing a reliable product is really important to us. As of December 2020, the framework is covered by more than 2100 tests."}),"\n",(0,i.jsx)(t.h3,{id:"documentation",children:"Documentation"}),"\n",(0,i.jsx)(t.p,{children:"New features, no matter what they offer, are useless if they are not well documented. Maintaining complete and quality documentation is key to the framework. If you think something is missing or unclear, feel free to open an issue on Github!"}),"\n",(0,i.jsx)(t.h3,{id:"product-stability",children:"Product Stability"}),"\n",(0,i.jsxs)(t.p,{children:["Great attention is paid to the stability of the product. You can find out more by consulting our ",(0,i.jsx)(t.a,{href:"https://github.com/FoalTS/foal/blob/master/.github/CONTRIBUTING.MD#dependency-policy",children:"dependency policy"}),", ",(0,i.jsx)(t.a,{href:"https://github.com/FoalTS/foal/blob/master/.github/CONTRIBUTING.MD#semantic-versioning",children:"semantic versioning rules"})," and ",(0,i.jsx)(t.a,{href:"https://github.com/FoalTS/foal/blob/master/.github/CONTRIBUTING.MD#long-term-support-policy-and-schedule",children:"long-term support policy"}),"."]}),"\n",(0,i.jsx)(t.h2,{id:"get-started",children:"Get Started"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"> npm install -g @foal/cli\n> foal createapp my-app\n> cd my-app\n> npm run dev\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The development server is started! Go to ",(0,i.jsx)(t.code,{children:"http://localhost:3001"})," and find our welcoming page!"]}),"\n",(0,i.jsxs)(t.p,{children:["\ud83d\udc49 ",(0,i.jsx)(t.a,{href:"./tutorials/simple-todo-list/1-installation",children:"Continue with the tutorial"})," \ud83c\udf31"]})]})}function h(e={}){const{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},28453:(e,t,o)=>{o.d(t,{R:()=>a,x:()=>l});var i=o(96540);const n={},s=i.createContext(n);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2278],{94842:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>r,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var i=o(74848),n=o(28453);const s={title:"Introduction",slug:"/"},a=void 0,l={id:"README",title:"Introduction",description:"License: MIT",source:"@site/docs/README.md",sourceDirName:".",slug:"/",permalink:"/docs/",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/README.md",tags:[],version:"current",frontMatter:{title:"Introduction",slug:"/"},sidebar:"someSidebar",next:{title:"Installation",permalink:"/docs/tutorials/simple-todo-list/1-installation"}},r={},c=[{value:"What is Foal?",id:"what-is-foal",level:2},{value:"Development Policy",id:"development-policy",level:2},{value:"Thousands of Tests",id:"thousands-of-tests",level:3},{value:"Documentation",id:"documentation",level:3},{value:"Product Stability",id:"product-stability",level:3},{value:"Get Started",id:"get-started",level:2}];function d(e){const t={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,n.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.img,{src:"https://img.shields.io/badge/License-MIT-blue.svg",alt:"License: MIT"}),"\n",(0,i.jsx)(t.img,{src:"https://snyk.io/test/github/foalts/foal/badge.svg",alt:"Known Vulnerabilities"}),"\n",(0,i.jsx)(t.img,{src:"https://img.shields.io/github/commit-activity/y/FoalTS/foal.svg",alt:"Commit activity"}),"\n",(0,i.jsx)(t.img,{src:"https://img.shields.io/github/last-commit/FoalTS/foal.svg",alt:"Last commit"})]}),"\n",(0,i.jsx)(t.h2,{id:"what-is-foal",children:"What is Foal?"}),"\n",(0,i.jsxs)(t.p,{children:[(0,i.jsx)(t.em,{children:"Foal"})," (or ",(0,i.jsx)(t.em,{children:"FoalTS"}),") is a Node.JS framework for creating web applications."]}),"\n",(0,i.jsx)(t.p,{children:"It provides a set of ready-to-use components so you don't have to reinvent the wheel every time. In one single place, you have a complete environment to build web applications. This includes a CLI, testing tools, frontend utilities, scripts, advanced authentication, ORM, deployment environments, GraphQL and Swagger API, AWS utilities, and more. You no longer need to get lost on npm searching for packages and making them work together. All is provided."}),"\n",(0,i.jsx)(t.p,{children:"But while offering all these features, the framework remains simple. Complexity and unnecessary abstractions are put aside to provide the most intuitive and expressive syntax. We believe that concise and elegant code is the best way to develop an application and maintain it in the future. It also allows you to spend more time coding rather than trying to understand how the framework works."}),"\n",(0,i.jsx)(t.p,{children:"Finally, the framework is entirely written in TypeScript. The language brings you optional static type-checking along with the latest ECMAScript features. This allows you to detect most silly errors during compilation and improve the quality of your code. It also offers you autocompletion and a well documented API."}),"\n",(0,i.jsx)(t.h2,{id:"development-policy",children:"Development Policy"}),"\n",(0,i.jsx)(t.h3,{id:"thousands-of-tests",children:"Thousands of Tests"}),"\n",(0,i.jsx)(t.p,{children:"Testing FoalTS is put on a very high priority. Providing a reliable product is really important to us. As of December 2020, the framework is covered by more than 2100 tests."}),"\n",(0,i.jsx)(t.h3,{id:"documentation",children:"Documentation"}),"\n",(0,i.jsx)(t.p,{children:"New features, no matter what they offer, are useless if they are not well documented. Maintaining complete and quality documentation is key to the framework. If you think something is missing or unclear, feel free to open an issue on Github!"}),"\n",(0,i.jsx)(t.h3,{id:"product-stability",children:"Product Stability"}),"\n",(0,i.jsxs)(t.p,{children:["Great attention is paid to the stability of the product. You can find out more by consulting our ",(0,i.jsx)(t.a,{href:"https://github.com/FoalTS/foal/blob/master/.github/CONTRIBUTING.MD#dependency-policy",children:"dependency policy"}),", ",(0,i.jsx)(t.a,{href:"https://github.com/FoalTS/foal/blob/master/.github/CONTRIBUTING.MD#semantic-versioning",children:"semantic versioning rules"})," and ",(0,i.jsx)(t.a,{href:"https://github.com/FoalTS/foal/blob/master/.github/CONTRIBUTING.MD#long-term-support-policy-and-schedule",children:"long-term support policy"}),"."]}),"\n",(0,i.jsx)(t.h2,{id:"get-started",children:"Get Started"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{children:"npx @foal/cli createapp my-app\ncd my-app\nnpm run dev\n"})}),"\n",(0,i.jsxs)(t.p,{children:["The development server is started! Go to ",(0,i.jsx)(t.code,{children:"http://localhost:3001"})," and find our welcoming page!"]}),"\n",(0,i.jsxs)(t.p,{children:["\ud83d\udc49 ",(0,i.jsx)(t.a,{href:"./tutorials/simple-todo-list/1-installation",children:"Continue with the tutorial"})," \ud83c\udf31"]})]})}function h(e={}){const{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},28453:(e,t,o)=>{o.d(t,{R:()=>a,x:()=>l});var i=o(96540);const n={},s=i.createContext(n);function a(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/297e34ad.040ed245.js b/assets/js/297e34ad.040ed245.js deleted file mode 100644 index a785c3ffba..0000000000 --- a/assets/js/297e34ad.040ed245.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5186],{70675:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>a,toc:()=>d});var r=n(74848),i=n(28453);const t={title:"Groups and Permissions",sidebar_label:"Groups & Permissions"},o=void 0,a={id:"authorization/groups-and-permissions",title:"Groups and Permissions",description:"In advanced applications, access control can be managed through permissions and groups.",source:"@site/docs/authorization/groups-and-permissions.md",sourceDirName:"authorization",slug:"/authorization/groups-and-permissions",permalink:"/docs/authorization/groups-and-permissions",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authorization/groups-and-permissions.md",tags:[],version:"current",frontMatter:{title:"Groups and Permissions",sidebar_label:"Groups & Permissions"},sidebar:"someSidebar",previous:{title:"Administrators & Roles",permalink:"/docs/authorization/administrators-and-roles"},next:{title:"Single Page Applications",permalink:"/docs/frontend/single-page-applications"}},c={},d=[{value:"Permissions",id:"permissions",level:2},{value:"The Permission Entity",id:"the-permission-entity",level:3},{value:"Creating Permissions Programmatically",id:"creating-permissions-programmatically",level:3},{value:"Creating Permissions with a Shell Script (CLI)",id:"creating-permissions-with-a-shell-script-cli",level:3},{value:"Groups",id:"groups",level:2},{value:"The Group Entity",id:"the-group-entity",level:3},{value:"Creating Groups Programmatically",id:"creating-groups-programmatically",level:3},{value:"Creating Groups with a Shell Script (CLI)",id:"creating-groups-with-a-shell-script-cli",level:3},{value:"Users",id:"users",level:2},{value:"The UserWithPermissions Entity",id:"the-userwithpermissions-entity",level:3},{value:"The hasPerm Method",id:"the-hasperm-method",level:3},{value:"The static findOneWithPermissionsBy Method",id:"the-static-findonewithpermissionsby-method",level:3},{value:"Creating Users with Groups and Permissions with a Shell Script (CLI)",id:"creating-users-with-groups-and-permissions-with-a-shell-script-cli",level:3},{value:"Fetching a User with their Permissions",id:"fetching-a-user-with-their-permissions",level:2},{value:"The PermissionRequired Hook",id:"the-permissionrequired-hook",level:2},{value:"BaseEntity Inheritance",id:"baseentity-inheritance",level:2},{value:"Get All Users with a Given Permission",id:"get-all-users-with-a-given-permission",level:2}];function l(e){const s={blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.p,{children:"In advanced applications, access control can be managed through permissions and groups."}),"\n",(0,r.jsxs)(s.p,{children:["A ",(0,r.jsx)(s.em,{children:"permission"})," gives a user the right to perform a given action (such as accessing a route)."]}),"\n",(0,r.jsxs)(s.p,{children:["A ",(0,r.jsx)(s.em,{children:"group"})," brings together a set of users (a user can belong to more than one group)."]}),"\n",(0,r.jsx)(s.p,{children:"Permissions can be attached to a user or a group. Attaching a permission to a group is equivalent to attaching the permission to each of its users."}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Examples of ",(0,r.jsx)(s.em,{children:"groups"}),' are the "Free", "Pro" and "Enterprise" plans of a SaaS application. Depending of the price paid by the customers, they have access to certain features whose access are managed by ',(0,r.jsx)(s.em,{children:"permissions"}),"."]}),"\n"]}),"\n",(0,r.jsx)(s.h2,{id:"permissions",children:"Permissions"}),"\n",(0,r.jsxs)(s.h3,{id:"the-permission-entity",children:["The ",(0,r.jsx)(s.code,{children:"Permission"})," Entity"]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Property name"}),(0,r.jsx)(s.th,{children:"Type"}),(0,r.jsx)(s.th,{children:"Database Link"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"id"}),(0,r.jsx)(s.td,{children:"number"}),(0,r.jsx)(s.td,{children:"Primary auto generated key"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"name"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"codeName"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{children:"Unique, Length: 100"})]})]})]}),"\n",(0,r.jsx)(s.h3,{id:"creating-permissions-programmatically",children:"Creating Permissions Programmatically"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Permission } from './src/app/entities';\n\nasync function main() {\n const perm = new Permission();\n perm.codeName = 'secret-perm';\n perm.name = 'Permission to access the secret';\n await perm.save();\n}\n"})}),"\n",(0,r.jsx)(s.h3,{id:"creating-permissions-with-a-shell-script-cli",children:"Creating Permissions with a Shell Script (CLI)"}),"\n",(0,r.jsx)(s.p,{children:"Create a new script with this command:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"foal generate script create-perm\n"})}),"\n",(0,r.jsxs)(s.p,{children:["Replace the content of the new created file ",(0,r.jsx)(s.code,{children:"src/scripts/create-perm.ts"})," with the following:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { Permission } from '@foal/typeorm';\n\n// App\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n codeName: { type: 'string', maxLength: 100 },\n name: { type: 'string' },\n },\n required: [ 'name', 'codeName' ],\n type: 'object',\n};\n\nexport async function main(args: { codeName: string, name: string }) {\n const permission = new Permission();\n permission.codeName = args.codeName;\n permission.name = args.name;\n\n await dataSource.initialize();\n\n try {\n console.log(\n await permission.save()\n );\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n"})}),"\n",(0,r.jsx)(s.p,{children:"Then you can create a permission through the command line."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-sh",children:'npm run build\nfoal run create-perm name="Permission to access the secret" codeName="access-secret"\n'})}),"\n",(0,r.jsx)(s.h2,{id:"groups",children:"Groups"}),"\n",(0,r.jsx)(s.p,{children:"Groups are used to categorize users. A user can belong to several groups and a group can have several users."}),"\n",(0,r.jsx)(s.p,{children:"A group can have permissions. They then apply to all its users."}),"\n",(0,r.jsx)(s.h3,{id:"the-group-entity",children:"The Group Entity"}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Property name"}),(0,r.jsx)(s.th,{children:"Type"}),(0,r.jsx)(s.th,{children:"Database Link"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"id"}),(0,r.jsx)(s.td,{children:"number"}),(0,r.jsx)(s.td,{children:"Primary auto generated key"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"name"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{children:"Length: 80"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"codeName"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{children:"Unique, Length: 100"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"permissions"}),(0,r.jsx)(s.td,{children:"Permission[]"}),(0,r.jsx)(s.td,{children:"A many-to-many relation with the table permission"})]})]})]}),"\n",(0,r.jsx)(s.h3,{id:"creating-groups-programmatically",children:"Creating Groups Programmatically"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Group, Permission } from './src/app/entities';\n\nasync function main() {\n const perm = new Permission();\n perm.codeName = 'delete-users';\n perm.name = 'Permission to delete users';\n await perm.save();\n\n const group = new Group();\n group.codeName = 'admin';\n group.name = 'Administrators';\n group.permissions = [ perm ];\n await group.save();\n}\n"})}),"\n",(0,r.jsx)(s.h3,{id:"creating-groups-with-a-shell-script-cli",children:"Creating Groups with a Shell Script (CLI)"}),"\n",(0,r.jsx)(s.p,{children:"Create a new script with this command:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"foal generate script create-group\n"})}),"\n",(0,r.jsxs)(s.p,{children:["Replace the content of the new created file ",(0,r.jsx)(s.code,{children:"src/scripts/create-group.ts"})," with the following:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { Group, Permission } from '@foal/typeorm';\n\n// App\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n codeName: { type: 'string', maxLength: 100 },\n name: { type: 'string', maxLength: 80 },\n permissions: { type: 'array', items: { type: 'string' }, uniqueItems: true, default: [] }\n },\n required: [ 'name', 'codeName' ],\n type: 'object',\n};\n\nexport async function main(args: { codeName: string, name: string, permissions: string[] }) {\n const group = new Group();\n group.permissions = [];\n group.codeName = args.codeName;\n group.name = args.name;\n\n await dataSource.initialize();\n\n try {\n for (const codeName of args.permissions) {\n const permission = await Permission.findOneBy({ codeName });\n if (!permission) {\n console.log(`No permission with the code name \"${codeName}\" was found.`);\n return;\n }\n group.permissions.push(permission);\n }\n\n console.log(\n await group.save()\n );\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,r.jsx)(s.p,{children:"Then you can create a group through the command line."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-sh",children:'npm run build\nfoal run create-perm name="Permission to delete users" codeName="delete-users"\nfoal run create-group name="Administrators" codeName="admin" permissions="[ \\"delete-users\\" ]"\n'})}),"\n",(0,r.jsx)(s.h2,{id:"users",children:"Users"}),"\n",(0,r.jsxs)(s.h3,{id:"the-userwithpermissions-entity",children:["The ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," Entity"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { UserWithPermissions } from '@foal/typeorm';\nimport { Entity } from 'typeorm';\n\n@Entity()\nexport class User extends UserWithPermissions {\n\n}\n\n// You MUST export Group and Permission so that TypeORM can generate migrations.\nexport { Group, Permission } from '@foal/typeorm';\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"UserWithPermissions"})," is an abstract class that has useful features to handle access control through permissions and groups. You must extend your ",(0,r.jsx)(s.code,{children:"User"})," entity from this class to use permissions and groups."]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Property name"}),(0,r.jsx)(s.th,{children:"Type"}),(0,r.jsx)(s.th,{children:"Database Link"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"id"}),(0,r.jsx)(s.td,{children:"number"}),(0,r.jsx)(s.td,{children:"Primary auto generated key"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"groups"}),(0,r.jsx)(s.td,{children:"Group[]"}),(0,r.jsx)(s.td,{children:"A many-to-many relation with the table group"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"userPermissions"}),(0,r.jsx)(s.td,{children:"Permission[]"}),(0,r.jsx)(s.td,{children:"A many-to-many relation with the table permission"})]})]})]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.img,{alt:"Relations between Users, Groups and Permissions",src:n(77319).A+"",width:"480",height:"280"})}),"\n",(0,r.jsxs)(s.h3,{id:"the-hasperm-method",children:["The ",(0,r.jsx)(s.code,{children:"hasPerm"})," Method"]}),"\n",(0,r.jsxs)(s.p,{children:["The ",(0,r.jsx)(s.code,{children:"hasPerm(permissionCodeName: string)"})," method of the ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," class returns true if one of these conditions is true:"]}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"The user has the required permission."}),"\n",(0,r.jsx)(s.li,{children:"The user belongs to a group that has the required permission."}),"\n"]}),"\n",(0,r.jsxs)(s.h3,{id:"the-static-findonewithpermissionsby-method",children:["The static ",(0,r.jsx)(s.code,{children:"findOneWithPermissionsBy"})," Method"]}),"\n",(0,r.jsxs)(s.p,{children:["This method takes an id as parameter and returns the corresponding user with its groups and permissions. If no user is found, the method returns ",(0,r.jsx)(s.code,{children:"null"}),"."]}),"\n",(0,r.jsx)(s.h3,{id:"creating-users-with-groups-and-permissions-with-a-shell-script-cli",children:"Creating Users with Groups and Permissions with a Shell Script (CLI)"}),"\n",(0,r.jsxs)(s.p,{children:["Replace the content of the new created file ",(0,r.jsx)(s.code,{children:"src/scripts/create-user.ts"})," with the following:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { hashPassword } from '@foal/core';\nimport { Group, Permission } from '@foal/typeorm';\n\n// App\nimport { User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n groups: { type: 'array', items: { type: 'string' }, uniqueItems: true, default: [] },\n password: { type: 'string' },\n userPermissions: { type: 'array', items: { type: 'string' }, uniqueItems: true, default: [] },\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport async function main(args) {\n const user = new User();\n user.userPermissions = [];\n user.groups = [];\n user.email = args.email;\n user.password = await hashPassword(args.password);\n\n await dataSource.initialize();\n\n for (const codeName of args.userPermissions as string[]) {\n const permission = await Permission.findOneBy({ codeName });\n if (!permission) {\n console.log(`No permission with the code name \"${codeName}\" was found.`);\n return;\n }\n user.userPermissions.push(permission);\n }\n\n for (const codeName of args.groups as string[]) {\n const group = await Group.findOneBy({ codeName });\n if (!group) {\n console.log(`No group with the code name \"${codeName}\" was found.`);\n return;\n }\n user.groups.push(group);\n }\n\n try {\n console.log(\n await user.save()\n );\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n"})}),"\n",(0,r.jsx)(s.p,{children:"Then you can create a user with their permissions and groups through the command line."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-sh",children:'npm run build\nfoal run create-user userPermissions="[ \\"my-first-perm\\" ]" groups="[ \\"my-group\\" ]"\n'})}),"\n",(0,r.jsx)(s.h2,{id:"fetching-a-user-with-their-permissions",children:"Fetching a User with their Permissions"}),"\n",(0,r.jsxs)(s.p,{children:["If you want the ",(0,r.jsx)(s.code,{children:"hasPerm"})," method to work on the context ",(0,r.jsx)(s.code,{children:"user"})," property, you must use the ",(0,r.jsx)(s.code,{children:"User.findOneWithPermissionsBy"})," method in the authentication hook."]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example with JSON Web Tokens"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n user: (id: number) => User.findOneWithPermissionsBy({ id })\n})\nexport class ProductController {\n @Get('/products')\n readProduct(ctx: Context) {\n if (!ctx.user.hasPerm('read-products')) {\n return new HttpResponseForbidden();\n }\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example with Sessions Tokens"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Context, Get, UseSessions } from '@foal/core';\n\n@UseSessions({\n required: true,\n user: (id: number) => User.findOneWithPermissionsBy({ id }),\n})\nexport class ProductController {\n @Get('/products')\n readProduct(ctx: Context) {\n if (!ctx.user.hasPerm('read-products')) {\n return new HttpResponseForbidden();\n }\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,r.jsx)(s.h2,{id:"the-permissionrequired-hook",children:"The PermissionRequired Hook"}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["This requires the use of ",(0,r.jsx)(s.code,{children:"User.findOneWithPermissionsBy"}),"."]}),"\n"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { PermissionRequired } from '@foal/core';\n\n@PermissionRequired('perm')\n"})}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Context"}),(0,r.jsx)(s.th,{children:"Response"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user"})," is null"]}),(0,r.jsx)(s.td,{children:"401 - UNAUTHORIZED"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user.hasPerm('perm')"})," is false"]}),(0,r.jsx)(s.td,{children:"403 - FORBIDDEN"})]})]})]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { PermissionRequired } from '@foal/core';\n\n@PermissionRequired('perm', { redirect: '/login' })\n"})}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Context"}),(0,r.jsx)(s.th,{children:"Response"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user"})," is null"]}),(0,r.jsxs)(s.td,{children:["Redirects to ",(0,r.jsx)(s.code,{children:"/login"})," (302 - FOUND)"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user.hasPerm('perm')"})," is false"]}),(0,r.jsx)(s.td,{children:"403 - FORBIDDEN"})]})]})]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Context, Get, PermissionRequired } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({ user: (id: number) => User.findOneWithPermissionsBy({ id }) })\nexport class ProductController {\n @Get('/products')\n @PermissionRequired('read-products')\n readProduct(ctx: Context) {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,r.jsx)(s.h2,{id:"baseentity-inheritance",children:"BaseEntity Inheritance"}),"\n",(0,r.jsxs)(s.p,{children:["The classes ",(0,r.jsx)(s.code,{children:"Permission"}),", ",(0,r.jsx)(s.code,{children:"Group"})," and ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," all extends the ",(0,r.jsx)(s.code,{children:"BaseEntity"})," class so you can access its static and instance methods."]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"const perm = await Permission.findOneByOrFail({ codeName: 'perm1' });\nperm.name = 'Permission1';\nawait perm.save();\n"})}),"\n",(0,r.jsx)(s.h2,{id:"get-all-users-with-a-given-permission",children:"Get All Users with a Given Permission"}),"\n",(0,r.jsxs)(s.p,{children:["The class ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," provides a static method ",(0,r.jsx)(s.code,{children:"withPerm"})," to get all users with a given permission. It returns all users that have this permission on their own or through the groups they belong to."]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"@Entity()\nclass User extends UserWithPermissions {}\n \nconst users = await User.withPerm('perm1');\n"})})]})}function h(e={}){const{wrapper:s}={...(0,i.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},77319:(e,s,n)=>{n.d(s,{A:()=>r});const r=n.p+"assets/images/permissions-groups-and-users-ec7a479e022323aca7ea069ba9622d31.png"},28453:(e,s,n)=>{n.d(s,{R:()=>o,x:()=>a});var r=n(96540);const i={},t=r.createContext(i);function o(e){const s=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function a(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(t.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/297e34ad.69f707ec.js b/assets/js/297e34ad.69f707ec.js new file mode 100644 index 0000000000..a8c9e5f089 --- /dev/null +++ b/assets/js/297e34ad.69f707ec.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5186],{70675:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>a,toc:()=>d});var r=n(74848),i=n(28453);const t={title:"Groups and Permissions",sidebar_label:"Groups & Permissions"},o=void 0,a={id:"authorization/groups-and-permissions",title:"Groups and Permissions",description:"In advanced applications, access control can be managed through permissions and groups.",source:"@site/docs/authorization/groups-and-permissions.md",sourceDirName:"authorization",slug:"/authorization/groups-and-permissions",permalink:"/docs/authorization/groups-and-permissions",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authorization/groups-and-permissions.md",tags:[],version:"current",frontMatter:{title:"Groups and Permissions",sidebar_label:"Groups & Permissions"},sidebar:"someSidebar",previous:{title:"Administrators & Roles",permalink:"/docs/authorization/administrators-and-roles"},next:{title:"Single Page Applications",permalink:"/docs/frontend/single-page-applications"}},c={},d=[{value:"Permissions",id:"permissions",level:2},{value:"The Permission Entity",id:"the-permission-entity",level:3},{value:"Creating Permissions Programmatically",id:"creating-permissions-programmatically",level:3},{value:"Creating Permissions with a Shell Script (CLI)",id:"creating-permissions-with-a-shell-script-cli",level:3},{value:"Groups",id:"groups",level:2},{value:"The Group Entity",id:"the-group-entity",level:3},{value:"Creating Groups Programmatically",id:"creating-groups-programmatically",level:3},{value:"Creating Groups with a Shell Script (CLI)",id:"creating-groups-with-a-shell-script-cli",level:3},{value:"Users",id:"users",level:2},{value:"The UserWithPermissions Entity",id:"the-userwithpermissions-entity",level:3},{value:"The hasPerm Method",id:"the-hasperm-method",level:3},{value:"The static findOneWithPermissionsBy Method",id:"the-static-findonewithpermissionsby-method",level:3},{value:"Creating Users with Groups and Permissions with a Shell Script (CLI)",id:"creating-users-with-groups-and-permissions-with-a-shell-script-cli",level:3},{value:"Fetching a User with their Permissions",id:"fetching-a-user-with-their-permissions",level:2},{value:"The PermissionRequired Hook",id:"the-permissionrequired-hook",level:2},{value:"BaseEntity Inheritance",id:"baseentity-inheritance",level:2},{value:"Get All Users with a Given Permission",id:"get-all-users-with-a-given-permission",level:2}];function l(e){const s={blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.p,{children:"In advanced applications, access control can be managed through permissions and groups."}),"\n",(0,r.jsxs)(s.p,{children:["A ",(0,r.jsx)(s.em,{children:"permission"})," gives a user the right to perform a given action (such as accessing a route)."]}),"\n",(0,r.jsxs)(s.p,{children:["A ",(0,r.jsx)(s.em,{children:"group"})," brings together a set of users (a user can belong to more than one group)."]}),"\n",(0,r.jsx)(s.p,{children:"Permissions can be attached to a user or a group. Attaching a permission to a group is equivalent to attaching the permission to each of its users."}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["Examples of ",(0,r.jsx)(s.em,{children:"groups"}),' are the "Free", "Pro" and "Enterprise" plans of a SaaS application. Depending of the price paid by the customers, they have access to certain features whose access are managed by ',(0,r.jsx)(s.em,{children:"permissions"}),"."]}),"\n"]}),"\n",(0,r.jsx)(s.h2,{id:"permissions",children:"Permissions"}),"\n",(0,r.jsxs)(s.h3,{id:"the-permission-entity",children:["The ",(0,r.jsx)(s.code,{children:"Permission"})," Entity"]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Property name"}),(0,r.jsx)(s.th,{children:"Type"}),(0,r.jsx)(s.th,{children:"Database Link"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"id"}),(0,r.jsx)(s.td,{children:"number"}),(0,r.jsx)(s.td,{children:"Primary auto generated key"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"name"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"codeName"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{children:"Unique, Length: 100"})]})]})]}),"\n",(0,r.jsx)(s.h3,{id:"creating-permissions-programmatically",children:"Creating Permissions Programmatically"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Permission } from './src/app/entities';\n\nasync function main() {\n const perm = new Permission();\n perm.codeName = 'secret-perm';\n perm.name = 'Permission to access the secret';\n await perm.save();\n}\n"})}),"\n",(0,r.jsx)(s.h3,{id:"creating-permissions-with-a-shell-script-cli",children:"Creating Permissions with a Shell Script (CLI)"}),"\n",(0,r.jsx)(s.p,{children:"Create a new script with this command:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"npx foal generate script create-perm\n"})}),"\n",(0,r.jsxs)(s.p,{children:["Replace the content of the new created file ",(0,r.jsx)(s.code,{children:"src/scripts/create-perm.ts"})," with the following:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { Permission } from '@foal/typeorm';\n\n// App\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n codeName: { type: 'string', maxLength: 100 },\n name: { type: 'string' },\n },\n required: [ 'name', 'codeName' ],\n type: 'object',\n};\n\nexport async function main(args: { codeName: string, name: string }) {\n const permission = new Permission();\n permission.codeName = args.codeName;\n permission.name = args.name;\n\n await dataSource.initialize();\n\n try {\n console.log(\n await permission.save()\n );\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n"})}),"\n",(0,r.jsx)(s.p,{children:"Then you can create a permission through the command line."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-sh",children:'npm run build\nnpx foal run create-perm name="Permission to access the secret" codeName="access-secret"\n'})}),"\n",(0,r.jsx)(s.h2,{id:"groups",children:"Groups"}),"\n",(0,r.jsx)(s.p,{children:"Groups are used to categorize users. A user can belong to several groups and a group can have several users."}),"\n",(0,r.jsx)(s.p,{children:"A group can have permissions. They then apply to all its users."}),"\n",(0,r.jsx)(s.h3,{id:"the-group-entity",children:"The Group Entity"}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Property name"}),(0,r.jsx)(s.th,{children:"Type"}),(0,r.jsx)(s.th,{children:"Database Link"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"id"}),(0,r.jsx)(s.td,{children:"number"}),(0,r.jsx)(s.td,{children:"Primary auto generated key"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"name"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{children:"Length: 80"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"codeName"}),(0,r.jsx)(s.td,{children:"string"}),(0,r.jsx)(s.td,{children:"Unique, Length: 100"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"permissions"}),(0,r.jsx)(s.td,{children:"Permission[]"}),(0,r.jsx)(s.td,{children:"A many-to-many relation with the table permission"})]})]})]}),"\n",(0,r.jsx)(s.h3,{id:"creating-groups-programmatically",children:"Creating Groups Programmatically"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Group, Permission } from './src/app/entities';\n\nasync function main() {\n const perm = new Permission();\n perm.codeName = 'delete-users';\n perm.name = 'Permission to delete users';\n await perm.save();\n\n const group = new Group();\n group.codeName = 'admin';\n group.name = 'Administrators';\n group.permissions = [ perm ];\n await group.save();\n}\n"})}),"\n",(0,r.jsx)(s.h3,{id:"creating-groups-with-a-shell-script-cli",children:"Creating Groups with a Shell Script (CLI)"}),"\n",(0,r.jsx)(s.p,{children:"Create a new script with this command:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{children:"npx foal generate script create-group\n"})}),"\n",(0,r.jsxs)(s.p,{children:["Replace the content of the new created file ",(0,r.jsx)(s.code,{children:"src/scripts/create-group.ts"})," with the following:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { Group, Permission } from '@foal/typeorm';\n\n// App\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n codeName: { type: 'string', maxLength: 100 },\n name: { type: 'string', maxLength: 80 },\n permissions: { type: 'array', items: { type: 'string' }, uniqueItems: true, default: [] }\n },\n required: [ 'name', 'codeName' ],\n type: 'object',\n};\n\nexport async function main(args: { codeName: string, name: string, permissions: string[] }) {\n const group = new Group();\n group.permissions = [];\n group.codeName = args.codeName;\n group.name = args.name;\n\n await dataSource.initialize();\n\n try {\n for (const codeName of args.permissions) {\n const permission = await Permission.findOneBy({ codeName });\n if (!permission) {\n console.log(`No permission with the code name \"${codeName}\" was found.`);\n return;\n }\n group.permissions.push(permission);\n }\n\n console.log(\n await group.save()\n );\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,r.jsx)(s.p,{children:"Then you can create a group through the command line."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-sh",children:'npm run build\nnpx foal run create-perm name="Permission to delete users" codeName="delete-users"\nnpx foal run create-group name="Administrators" codeName="admin" permissions="[ \\"delete-users\\" ]"\n'})}),"\n",(0,r.jsx)(s.h2,{id:"users",children:"Users"}),"\n",(0,r.jsxs)(s.h3,{id:"the-userwithpermissions-entity",children:["The ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," Entity"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { UserWithPermissions } from '@foal/typeorm';\nimport { Entity } from 'typeorm';\n\n@Entity()\nexport class User extends UserWithPermissions {\n\n}\n\n// You MUST export Group and Permission so that TypeORM can generate migrations.\nexport { Group, Permission } from '@foal/typeorm';\n"})}),"\n",(0,r.jsxs)(s.p,{children:[(0,r.jsx)(s.code,{children:"UserWithPermissions"})," is an abstract class that has useful features to handle access control through permissions and groups. You must extend your ",(0,r.jsx)(s.code,{children:"User"})," entity from this class to use permissions and groups."]}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Property name"}),(0,r.jsx)(s.th,{children:"Type"}),(0,r.jsx)(s.th,{children:"Database Link"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"id"}),(0,r.jsx)(s.td,{children:"number"}),(0,r.jsx)(s.td,{children:"Primary auto generated key"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"groups"}),(0,r.jsx)(s.td,{children:"Group[]"}),(0,r.jsx)(s.td,{children:"A many-to-many relation with the table group"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.td,{children:"userPermissions"}),(0,r.jsx)(s.td,{children:"Permission[]"}),(0,r.jsx)(s.td,{children:"A many-to-many relation with the table permission"})]})]})]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.img,{alt:"Relations between Users, Groups and Permissions",src:n(77319).A+"",width:"480",height:"280"})}),"\n",(0,r.jsxs)(s.h3,{id:"the-hasperm-method",children:["The ",(0,r.jsx)(s.code,{children:"hasPerm"})," Method"]}),"\n",(0,r.jsxs)(s.p,{children:["The ",(0,r.jsx)(s.code,{children:"hasPerm(permissionCodeName: string)"})," method of the ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," class returns true if one of these conditions is true:"]}),"\n",(0,r.jsxs)(s.ul,{children:["\n",(0,r.jsx)(s.li,{children:"The user has the required permission."}),"\n",(0,r.jsx)(s.li,{children:"The user belongs to a group that has the required permission."}),"\n"]}),"\n",(0,r.jsxs)(s.h3,{id:"the-static-findonewithpermissionsby-method",children:["The static ",(0,r.jsx)(s.code,{children:"findOneWithPermissionsBy"})," Method"]}),"\n",(0,r.jsxs)(s.p,{children:["This method takes an id as parameter and returns the corresponding user with its groups and permissions. If no user is found, the method returns ",(0,r.jsx)(s.code,{children:"null"}),"."]}),"\n",(0,r.jsx)(s.h3,{id:"creating-users-with-groups-and-permissions-with-a-shell-script-cli",children:"Creating Users with Groups and Permissions with a Shell Script (CLI)"}),"\n",(0,r.jsxs)(s.p,{children:["Replace the content of the new created file ",(0,r.jsx)(s.code,{children:"src/scripts/create-user.ts"})," with the following:"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { hashPassword } from '@foal/core';\nimport { Group, Permission } from '@foal/typeorm';\n\n// App\nimport { User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n groups: { type: 'array', items: { type: 'string' }, uniqueItems: true, default: [] },\n password: { type: 'string' },\n userPermissions: { type: 'array', items: { type: 'string' }, uniqueItems: true, default: [] },\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport async function main(args) {\n const user = new User();\n user.userPermissions = [];\n user.groups = [];\n user.email = args.email;\n user.password = await hashPassword(args.password);\n\n await dataSource.initialize();\n\n for (const codeName of args.userPermissions as string[]) {\n const permission = await Permission.findOneBy({ codeName });\n if (!permission) {\n console.log(`No permission with the code name \"${codeName}\" was found.`);\n return;\n }\n user.userPermissions.push(permission);\n }\n\n for (const codeName of args.groups as string[]) {\n const group = await Group.findOneBy({ codeName });\n if (!group) {\n console.log(`No group with the code name \"${codeName}\" was found.`);\n return;\n }\n user.groups.push(group);\n }\n\n try {\n console.log(\n await user.save()\n );\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n"})}),"\n",(0,r.jsx)(s.p,{children:"Then you can create a user with their permissions and groups through the command line."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-sh",children:'npm run build\nnpx foal run create-user userPermissions="[ \\"my-first-perm\\" ]" groups="[ \\"my-group\\" ]"\n'})}),"\n",(0,r.jsx)(s.h2,{id:"fetching-a-user-with-their-permissions",children:"Fetching a User with their Permissions"}),"\n",(0,r.jsxs)(s.p,{children:["If you want the ",(0,r.jsx)(s.code,{children:"hasPerm"})," method to work on the context ",(0,r.jsx)(s.code,{children:"user"})," property, you must use the ",(0,r.jsx)(s.code,{children:"User.findOneWithPermissionsBy"})," method in the authentication hook."]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example with JSON Web Tokens"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n user: (id: number) => User.findOneWithPermissionsBy({ id })\n})\nexport class ProductController {\n @Get('/products')\n readProduct(ctx: Context) {\n if (!ctx.user.hasPerm('read-products')) {\n return new HttpResponseForbidden();\n }\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example with Sessions Tokens"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Context, Get, UseSessions } from '@foal/core';\n\n@UseSessions({\n required: true,\n user: (id: number) => User.findOneWithPermissionsBy({ id }),\n})\nexport class ProductController {\n @Get('/products')\n readProduct(ctx: Context) {\n if (!ctx.user.hasPerm('read-products')) {\n return new HttpResponseForbidden();\n }\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,r.jsx)(s.h2,{id:"the-permissionrequired-hook",children:"The PermissionRequired Hook"}),"\n",(0,r.jsxs)(s.blockquote,{children:["\n",(0,r.jsxs)(s.p,{children:["This requires the use of ",(0,r.jsx)(s.code,{children:"User.findOneWithPermissionsBy"}),"."]}),"\n"]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { PermissionRequired } from '@foal/core';\n\n@PermissionRequired('perm')\n"})}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Context"}),(0,r.jsx)(s.th,{children:"Response"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user"})," is null"]}),(0,r.jsx)(s.td,{children:"401 - UNAUTHORIZED"})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user.hasPerm('perm')"})," is false"]}),(0,r.jsx)(s.td,{children:"403 - FORBIDDEN"})]})]})]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { PermissionRequired } from '@foal/core';\n\n@PermissionRequired('perm', { redirect: '/login' })\n"})}),"\n",(0,r.jsxs)(s.table,{children:[(0,r.jsx)(s.thead,{children:(0,r.jsxs)(s.tr,{children:[(0,r.jsx)(s.th,{children:"Context"}),(0,r.jsx)(s.th,{children:"Response"})]})}),(0,r.jsxs)(s.tbody,{children:[(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user"})," is null"]}),(0,r.jsxs)(s.td,{children:["Redirects to ",(0,r.jsx)(s.code,{children:"/login"})," (302 - FOUND)"]})]}),(0,r.jsxs)(s.tr,{children:[(0,r.jsxs)(s.td,{children:[(0,r.jsx)(s.code,{children:"ctx.user.hasPerm('perm')"})," is false"]}),(0,r.jsx)(s.td,{children:"403 - FORBIDDEN"})]})]})]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"import { Context, Get, PermissionRequired } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({ user: (id: number) => User.findOneWithPermissionsBy({ id }) })\nexport class ProductController {\n @Get('/products')\n @PermissionRequired('read-products')\n readProduct(ctx: Context) {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,r.jsx)(s.h2,{id:"baseentity-inheritance",children:"BaseEntity Inheritance"}),"\n",(0,r.jsxs)(s.p,{children:["The classes ",(0,r.jsx)(s.code,{children:"Permission"}),", ",(0,r.jsx)(s.code,{children:"Group"})," and ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," all extends the ",(0,r.jsx)(s.code,{children:"BaseEntity"})," class so you can access its static and instance methods."]}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Example"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"const perm = await Permission.findOneByOrFail({ codeName: 'perm1' });\nperm.name = 'Permission1';\nawait perm.save();\n"})}),"\n",(0,r.jsx)(s.h2,{id:"get-all-users-with-a-given-permission",children:"Get All Users with a Given Permission"}),"\n",(0,r.jsxs)(s.p,{children:["The class ",(0,r.jsx)(s.code,{children:"UserWithPermissions"})," provides a static method ",(0,r.jsx)(s.code,{children:"withPerm"})," to get all users with a given permission. It returns all users that have this permission on their own or through the groups they belong to."]}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-typescript",children:"@Entity()\nclass User extends UserWithPermissions {}\n \nconst users = await User.withPerm('perm1');\n"})})]})}function h(e={}){const{wrapper:s}={...(0,i.R)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},77319:(e,s,n)=>{n.d(s,{A:()=>r});const r=n.p+"assets/images/permissions-groups-and-users-ec7a479e022323aca7ea069ba9622d31.png"},28453:(e,s,n)=>{n.d(s,{R:()=>o,x:()=>a});var r=n(96540);const i={},t=r.createContext(i);function o(e){const s=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function a(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(t.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2e423443.cf2e52fc.js b/assets/js/2e423443.cf2e52fc.js deleted file mode 100644 index abd6b08d04..0000000000 --- a/assets/js/2e423443.cf2e52fc.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3416],{7017:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var o=n(74848),r=n(28453);const s={title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},a=void 0,i={permalink:"/blog/2024/04/25/version-4.4-release-notes",editUrl:"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-25-version-4.4-release-notes.md",source:"@site/blog/2024-04-25-version-4.4-release-notes.md",title:"Version 4.4 release notes",description:"Banner",date:"2024-04-25T00:00:00.000Z",formattedDate:"April 25, 2024",tags:[{label:"release",permalink:"/blog/tags/release"}],readingTime:.19,hasTruncateMarker:!0,authors:[{name:"Lo\xefc Poullain",title:"Creator of FoalTS. Software engineer.",url:"https://loicpoullain.com",imageURL:"https://avatars1.githubusercontent.com/u/13604533?v=4"}],frontMatter:{title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},unlisted:!1,nextItem:{title:"Version 4.3 release notes",permalink:"/blog/2024/04/16/version-4.3-release-notes"}},l={authorsImageUrls:[void 0]},c=[];function u(e){const t={a:"a",code:"code",img:"img",p:"p",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Banner",src:n(7195).A+"",width:"914",height:"315"})}),"\n",(0,o.jsxs)(t.p,{children:["Version 4.4 of ",(0,o.jsx)(t.a,{href:"https://foalts.org/",children:"Foal"})," is out!"]}),"\n",(0,o.jsxs)(t.p,{children:["This release updates Foal's sub-dependencies, including the ",(0,o.jsx)(t.code,{children:"express"})," library, which presents a moderate vulnerability in versions prior to 4.19.2."]}),"\n",(0,o.jsxs)(t.p,{children:["Thanks to ",(0,o.jsx)(t.a,{href:"https://github.com/lcnvdl",children:"Lucho"})," for reporting this vulnerability in the first place!"]})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(u,{...e})}):u(e)}},7195:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/images/banner-33e9e1af8b06d33e1bedef8fc0c5071c.png"},28453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>i});var o=n(96540);const r={},s=o.createContext(r);function a(e){const t=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),o.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2e423443.f2ab71bc.js b/assets/js/2e423443.f2ab71bc.js new file mode 100644 index 0000000000..d072363d70 --- /dev/null +++ b/assets/js/2e423443.f2ab71bc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3416],{7017:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var o=n(74848),r=n(28453);const s={title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},a=void 0,i={permalink:"/blog/2024/04/25/version-4.4-release-notes",editUrl:"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-25-version-4.4-release-notes.md",source:"@site/blog/2024-04-25-version-4.4-release-notes.md",title:"Version 4.4 release notes",description:"Banner",date:"2024-04-25T00:00:00.000Z",formattedDate:"April 25, 2024",tags:[{label:"release",permalink:"/blog/tags/release"}],readingTime:.19,hasTruncateMarker:!0,authors:[{name:"Lo\xefc Poullain",title:"Creator of FoalTS. Software engineer.",url:"https://loicpoullain.com",imageURL:"https://avatars1.githubusercontent.com/u/13604533?v=4"}],frontMatter:{title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},unlisted:!1,prevItem:{title:"Version 4.5 release notes",permalink:"/blog/2024/08/22/version-4.5-release-notes"},nextItem:{title:"Version 4.3 release notes",permalink:"/blog/2024/04/16/version-4.3-release-notes"}},l={authorsImageUrls:[void 0]},c=[];function u(e){const t={a:"a",code:"code",img:"img",p:"p",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Banner",src:n(7195).A+"",width:"914",height:"315"})}),"\n",(0,o.jsxs)(t.p,{children:["Version 4.4 of ",(0,o.jsx)(t.a,{href:"https://foalts.org/",children:"Foal"})," is out!"]}),"\n",(0,o.jsxs)(t.p,{children:["This release updates Foal's sub-dependencies, including the ",(0,o.jsx)(t.code,{children:"express"})," library, which presents a moderate vulnerability in versions prior to 4.19.2."]}),"\n",(0,o.jsxs)(t.p,{children:["Thanks to ",(0,o.jsx)(t.a,{href:"https://github.com/lcnvdl",children:"Lucho"})," for reporting this vulnerability in the first place!"]})]})}function h(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(u,{...e})}):u(e)}},7195:(e,t,n)=>{n.d(t,{A:()=>o});const o=n.p+"assets/images/banner-33e9e1af8b06d33e1bedef8fc0c5071c.png"},28453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>i});var o=n(96540);const r={},s=o.createContext(r);function a(e){const t=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),o.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2e47d16a.93ea4ddd.js b/assets/js/2e47d16a.93ea4ddd.js new file mode 100644 index 0000000000..050b7972c7 --- /dev/null +++ b/assets/js/2e47d16a.93ea4ddd.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2685],{49814:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>l});var n=o(74848),s=o(28453);const i={title:"The Shell Script create-todo",id:"tuto-4-the-shell-script-create-todo",slug:"4-the-shell-script-create-todo"},r=void 0,a={id:"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo",title:"The Shell Script create-todo",description:"Now it is time to populate the database with some tasks.",source:"@site/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",sourceDirName:"tutorials/simple-todo-list",slug:"/tutorials/simple-todo-list/4-the-shell-script-create-todo",permalink:"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"The Shell Script create-todo",id:"tuto-4-the-shell-script-create-todo",slug:"4-the-shell-script-create-todo"},sidebar:"someSidebar",previous:{title:"The Todo Model",permalink:"/docs/tutorials/simple-todo-list/3-the-todo-model"},next:{title:"The REST API",permalink:"/docs/tutorials/simple-todo-list/5-the-rest-api"}},c={},l=[];function d(e){const t={blockquote:"blockquote",code:"code",em:"em",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:"Now it is time to populate the database with some tasks."}),"\n",(0,n.jsxs)(t.p,{children:["You could run the command line interface of your database (in that case ",(0,n.jsx)(t.em,{children:"SQLite"}),") and enter some SQL queries. But this is risky and not very handy. It becomes especially true when the complexity of your models increases (relations many-to-many, etc)."]}),"\n",(0,n.jsxs)(t.p,{children:["That's why you are going to create and use a ",(0,n.jsx)(t.em,{children:"shell script"})," instead."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-sh",children:"npx foal generate script create-todo\n"})}),"\n",(0,n.jsxs)(t.p,{children:["A ",(0,n.jsx)(t.em,{children:"shell script"})," is a piece of code intended to be called from the command line. It has access to all the components of your application, including your models. A shell script is divided in two parts:"]}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["a ",(0,n.jsx)(t.code,{children:"schema"})," object which defines the specification of the command line arguments,"]}),"\n",(0,n.jsxs)(t.li,{children:["and a ",(0,n.jsx)(t.code,{children:"main"})," function which gets these arguments as an object and executes some code."]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Open the new generated file in the ",(0,n.jsx)(t.code,{children:"src/scripts"})," directory and update its content."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"// App\nimport { Todo } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n properties: {\n text: { type: 'string' }\n },\n required: [ 'text' ],\n type: 'object',\n};\n\nexport async function main(args: { text: string }) {\n // Connect to the database.\n await dataSource.initialize();\n\n try {\n // Create a new task with the text given in the command line.\n const todo = new Todo();\n todo.text = args.text;\n\n // Save the task in the database and then display it in the console.\n console.log(await todo.save());\n } catch (error: any) {\n console.log(error.message);\n } finally {\n // Close the connection to the database.\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,n.jsx)(t.p,{children:"Build the script."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-sh",children:"npm run build\n"})}),"\n",(0,n.jsx)(t.p,{children:"Then run the script to create tasks in the database."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-sh",children:'npx foal run create-todo text="Read the docs"\nnpx foal run create-todo text="Create my first application"\nnpx foal run create-todo text="Write tests"\n'})}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsx)(t.p,{children:"Note that if you try to create a new to-do without specifying the text argument, you'll get the error below."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.code,{children:"Script error: arguments must have required property 'text'."})}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},28453:(e,t,o)=>{o.d(t,{R:()=>r,x:()=>a});var n=o(96540);const s={},i=n.createContext(s);function r(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/2e47d16a.97a8bb29.js b/assets/js/2e47d16a.97a8bb29.js deleted file mode 100644 index d259d721c0..0000000000 --- a/assets/js/2e47d16a.97a8bb29.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2685],{49814:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>l});var n=o(74848),s=o(28453);const i={title:"The Shell Script create-todo",id:"tuto-4-the-shell-script-create-todo",slug:"4-the-shell-script-create-todo"},r=void 0,a={id:"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo",title:"The Shell Script create-todo",description:"Now it is time to populate the database with some tasks.",source:"@site/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",sourceDirName:"tutorials/simple-todo-list",slug:"/tutorials/simple-todo-list/4-the-shell-script-create-todo",permalink:"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"The Shell Script create-todo",id:"tuto-4-the-shell-script-create-todo",slug:"4-the-shell-script-create-todo"},sidebar:"someSidebar",previous:{title:"The Todo Model",permalink:"/docs/tutorials/simple-todo-list/3-the-todo-model"},next:{title:"The REST API",permalink:"/docs/tutorials/simple-todo-list/5-the-rest-api"}},c={},l=[];function d(e){const t={blockquote:"blockquote",code:"code",em:"em",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:"Now it is time to populate the database with some tasks."}),"\n",(0,n.jsxs)(t.p,{children:["You could run the command line interface of your database (in that case ",(0,n.jsx)(t.em,{children:"SQLite"}),") and enter some SQL queries. But this is risky and not very handy. It becomes especially true when the complexity of your models increases (relations many-to-many, etc)."]}),"\n",(0,n.jsxs)(t.p,{children:["That's why you are going to create and use a ",(0,n.jsx)(t.em,{children:"shell script"})," instead."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-sh",children:"foal generate script create-todo\n"})}),"\n",(0,n.jsxs)(t.p,{children:["A ",(0,n.jsx)(t.em,{children:"shell script"})," is a piece of code intended to be called from the command line. It has access to all the components of your application, including your models. A shell script is divided in two parts:"]}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["a ",(0,n.jsx)(t.code,{children:"schema"})," object which defines the specification of the command line arguments,"]}),"\n",(0,n.jsxs)(t.li,{children:["and a ",(0,n.jsx)(t.code,{children:"main"})," function which gets these arguments as an object and executes some code."]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Open the new generated file in the ",(0,n.jsx)(t.code,{children:"src/scripts"})," directory and update its content."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"// App\nimport { Todo } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n properties: {\n text: { type: 'string' }\n },\n required: [ 'text' ],\n type: 'object',\n};\n\nexport async function main(args: { text: string }) {\n // Connect to the database.\n await dataSource.initialize();\n\n try {\n // Create a new task with the text given in the command line.\n const todo = new Todo();\n todo.text = args.text;\n\n // Save the task in the database and then display it in the console.\n console.log(await todo.save());\n } catch (error: any) {\n console.log(error.message);\n } finally {\n // Close the connection to the database.\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,n.jsx)(t.p,{children:"Build the script."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-sh",children:"npm run build\n"})}),"\n",(0,n.jsx)(t.p,{children:"Then run the script to create tasks in the database."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-sh",children:'foal run create-todo text="Read the docs"\nfoal run create-todo text="Create my first application"\nfoal run create-todo text="Write tests"\n'})}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsx)(t.p,{children:"Note that if you try to create a new to-do without specifying the text argument, you'll get the error below."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.code,{children:"Script error: arguments must have required property 'text'."})}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},28453:(e,t,o)=>{o.d(t,{R:()=>r,x:()=>a});var n=o(96540);const s={},i=n.createContext(s);function r(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3fbe8240.b3c7f598.js b/assets/js/3fbe8240.b3c7f598.js deleted file mode 100644 index 49a21a63a4..0000000000 --- a/assets/js/3fbe8240.b3c7f598.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5953],{55874:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>i,default:()=>h,frontMatter:()=>l,metadata:()=>c,toc:()=>u});var r=t(74848),a=t(28453),o=t(11470),s=t(19365);const l={title:"MongoDB (noSQL)",sidebar_label:"NoSQL"},i=void 0,c={id:"databases/typeorm/mongodb",title:"MongoDB (noSQL)",description:"Creating a new project",source:"@site/docs/databases/typeorm/mongodb.md",sourceDirName:"databases/typeorm",slug:"/databases/typeorm/mongodb",permalink:"/docs/databases/typeorm/mongodb",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/databases/typeorm/mongodb.md",tags:[],version:"current",frontMatter:{title:"MongoDB (noSQL)",sidebar_label:"NoSQL"},sidebar:"someSidebar",previous:{title:"Migrations",permalink:"/docs/databases/typeorm/generate-and-run-migrations"},next:{title:"Introduction",permalink:"/docs/databases/other-orm/introduction"}},d={},u=[{value:"Creating a new project",id:"creating-a-new-project",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining Entities and Columns",id:"defining-entities-and-columns",level:2},{value:"Authentication",id:"authentication",level:2},{value:"The MongoDBStore",id:"the-mongodbstore",level:3},{value:"Limitations",id:"limitations",level:2}];function m(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h2,{id:"creating-a-new-project",children:"Creating a new project"}),"\n",(0,r.jsxs)(n.p,{children:["To generate a new project that uses MongoDB, run the command ",(0,r.jsx)(n.code,{children:"createapp"})," with the flag ",(0,r.jsx)(n.code,{children:"--mongodb"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"foal createapp my-app --mongodb\n"})}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install mongodb@5\n"})}),"\n",(0,r.jsxs)(o.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(s.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"database:\n type: mongodb\n url: mongodb://localhost:27017/test-foo-bar\n"})})}),(0,r.jsx)(s.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "database": {\n "type": "mongodb",\n "url": "mongodb://localhost:27017/test-foo-bar"\n }\n}\n'})})}),(0,r.jsx)(s.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n database: {\n type: "mongodb",\n url: "mongodb://localhost:27017/test-foo-bar"\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h2,{id:"defining-entities-and-columns",children:"Defining Entities and Columns"}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["More documentation here: ",(0,r.jsx)(n.a,{href:"https://github.com/typeorm/typeorm/blob/master/docs/mongodb.md",children:"https://github.com/typeorm/typeorm/blob/master/docs/mongodb.md"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The definition of entities and columns is the same as in relational databases, except that the ID type must be an ",(0,r.jsx)(n.code,{children:"ObjectID"})," and the column decorator must be ",(0,r.jsx)(n.code,{children:"@ObjectIdColumn"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Entity, ObjectID, ObjectIdColumn, Column } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n \n @ObjectIdColumn()\n _id: ObjectID;\n \n @Column()\n firstName: string;\n \n @Column()\n lastName: string;\n \n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { ObjectId } from 'mongodb';\n\nconst user = await User.findOneBy({ _id: new ObjectId('xxxx') });\n"})}),"\n",(0,r.jsx)(n.h2,{id:"authentication",children:"Authentication"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Entity, ObjectID, ObjectIdColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @ObjectIdColumn()\n _id: ObjectID;\n\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.em,{children:"Example with JSON Web Tokens"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\nimport { ObjectId } from 'mongodb';\n\nimport { User } from '../entities';\n\n@JWTRequired({\n userIdType: 'string',\n user: id => User.findOneBy({ _id: new ObjectId(id) })\n})\nclass MyController {}\n"})}),"\n",(0,r.jsxs)(n.h3,{id:"the-mongodbstore",children:["The ",(0,r.jsx)(n.code,{children:"MongoDBStore"})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install @foal/mongodb\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If you use sessions with ",(0,r.jsx)(n.code,{children:"@UseSessions"}),", you must use the ",(0,r.jsx)(n.code,{children:"MongoDBStore"})," from ",(0,r.jsx)(n.code,{children:"@foal/mongodb"}),". ",(0,r.jsxs)(n.strong,{children:["The ",(0,r.jsx)(n.code,{children:"TypeORMStore"})," does not work with noSQL databases."]})]}),"\n",(0,r.jsx)(n.h2,{id:"limitations",children:"Limitations"}),"\n",(0,r.jsx)(n.p,{children:"When using MongoDB, there are some features that are not available:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["the ",(0,r.jsx)(n.code,{children:"foal g rest-api "})," command,"]}),"\n",(0,r.jsxs)(n.li,{children:["and the ",(0,r.jsx)(n.em,{children:"Groups & Permissions"})," system."]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(m,{...e})}):m(e)}},19365:(e,n,t)=>{t.d(n,{A:()=>s});t(96540);var r=t(34164);const a={tabItem:"tabItem_Ymn6"};var o=t(74848);function s(e){let{children:n,hidden:t,className:s}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,r.A)(a.tabItem,s),hidden:t,children:n})}},11470:(e,n,t)=>{t.d(n,{A:()=>w});var r=t(96540),a=t(34164),o=t(23104),s=t(56347),l=t(205),i=t(57485),c=t(31682),d=t(89466);function u(e){return r.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function m(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:t,attributes:r,default:a}}=e;return{value:n,label:t,attributes:r,default:a}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function h(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function p(e){let{queryString:n=!1,groupId:t}=e;const a=(0,s.W6)(),o=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,i.aZ)(o),(0,r.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(a.location.search);n.set(o,e),a.replace({...a.location,search:n.toString()})}),[o,a])]}function b(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,o=m(e),[s,i]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!h({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:o}))),[c,u]=p({queryString:t,groupId:a}),[b,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,o]=(0,d.Dv)(t);return[a,(0,r.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:a}),g=(()=>{const e=c??b;return h({value:e,tabValues:o})?e:null})();(0,l.A)((()=>{g&&i(g)}),[g]);return{selectedValue:s,selectValue:(0,r.useCallback)((e=>{if(!h({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);i(e),u(e),f(e)}),[u,f,o]),tabValues:o}}var f=t(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=t(74848);function x(e){let{className:n,block:t,selectedValue:r,selectValue:s,tabValues:l}=e;const i=[],{blockElementScrollPositionUntilNextRender:c}=(0,o.a_)(),d=e=>{const n=e.currentTarget,t=i.indexOf(n),a=l[t].value;a!==r&&(c(n),s(a))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=i.indexOf(e.currentTarget)+1;n=i[t]??i[0];break}case"ArrowLeft":{const t=i.indexOf(e.currentTarget)-1;n=i[t]??i[i.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:o}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:r===n?0:-1,"aria-selected":r===n,ref:e=>i.push(e),onKeyDown:u,onClick:d,...o,className:(0,a.A)("tabs__item",g.tabItem,o?.className,{"tabs__item--active":r===n}),children:t??n},n)}))})}function v(e){let{lazy:n,children:t,selectedValue:a}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===a));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==a})))})}function y(e){const n=b(e);return(0,j.jsxs)("div",{className:(0,a.A)("tabs-container",g.tabList),children:[(0,j.jsx)(x,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function w(e){const n=(0,f.A)();return(0,j.jsx)(y,{...e,children:u(e.children)},String(n))}},28453:(e,n,t)=>{t.d(n,{R:()=>s,x:()=>l});var r=t(96540);const a={},o=r.createContext(a);function s(e){const n=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3fbe8240.fbff572e.js b/assets/js/3fbe8240.fbff572e.js new file mode 100644 index 0000000000..e2f76abb6b --- /dev/null +++ b/assets/js/3fbe8240.fbff572e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5953],{55874:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>i,default:()=>h,frontMatter:()=>l,metadata:()=>c,toc:()=>u});var r=t(74848),a=t(28453),o=t(11470),s=t(19365);const l={title:"MongoDB (noSQL)",sidebar_label:"NoSQL"},i=void 0,c={id:"databases/typeorm/mongodb",title:"MongoDB (noSQL)",description:"Creating a new project",source:"@site/docs/databases/typeorm/mongodb.md",sourceDirName:"databases/typeorm",slug:"/databases/typeorm/mongodb",permalink:"/docs/databases/typeorm/mongodb",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/databases/typeorm/mongodb.md",tags:[],version:"current",frontMatter:{title:"MongoDB (noSQL)",sidebar_label:"NoSQL"},sidebar:"someSidebar",previous:{title:"Migrations",permalink:"/docs/databases/typeorm/generate-and-run-migrations"},next:{title:"Introduction",permalink:"/docs/databases/other-orm/introduction"}},d={},u=[{value:"Creating a new project",id:"creating-a-new-project",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining Entities and Columns",id:"defining-entities-and-columns",level:2},{value:"Authentication",id:"authentication",level:2},{value:"The MongoDBStore",id:"the-mongodbstore",level:3},{value:"Limitations",id:"limitations",level:2}];function m(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h2,{id:"creating-a-new-project",children:"Creating a new project"}),"\n",(0,r.jsxs)(n.p,{children:["To generate a new project that uses MongoDB, run the command ",(0,r.jsx)(n.code,{children:"createapp"})," with the flag ",(0,r.jsx)(n.code,{children:"--mongodb"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npx @foal/cli createapp my-app --mongodb\n"})}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install mongodb@5\n"})}),"\n",(0,r.jsxs)(o.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(s.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"database:\n type: mongodb\n url: mongodb://localhost:27017/test-foo-bar\n"})})}),(0,r.jsx)(s.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "database": {\n "type": "mongodb",\n "url": "mongodb://localhost:27017/test-foo-bar"\n }\n}\n'})})}),(0,r.jsx)(s.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n database: {\n type: "mongodb",\n url: "mongodb://localhost:27017/test-foo-bar"\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h2,{id:"defining-entities-and-columns",children:"Defining Entities and Columns"}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["More documentation here: ",(0,r.jsx)(n.a,{href:"https://github.com/typeorm/typeorm/blob/master/docs/mongodb.md",children:"https://github.com/typeorm/typeorm/blob/master/docs/mongodb.md"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The definition of entities and columns is the same as in relational databases, except that the ID type must be an ",(0,r.jsx)(n.code,{children:"ObjectID"})," and the column decorator must be ",(0,r.jsx)(n.code,{children:"@ObjectIdColumn"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Entity, ObjectID, ObjectIdColumn, Column } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n \n @ObjectIdColumn()\n _id: ObjectID;\n \n @Column()\n firstName: string;\n \n @Column()\n lastName: string;\n \n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { ObjectId } from 'mongodb';\n\nconst user = await User.findOneBy({ _id: new ObjectId('xxxx') });\n"})}),"\n",(0,r.jsx)(n.h2,{id:"authentication",children:"Authentication"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Entity, ObjectID, ObjectIdColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @ObjectIdColumn()\n _id: ObjectID;\n\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.em,{children:"Example with JSON Web Tokens"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\nimport { ObjectId } from 'mongodb';\n\nimport { User } from '../entities';\n\n@JWTRequired({\n userIdType: 'string',\n user: id => User.findOneBy({ _id: new ObjectId(id) })\n})\nclass MyController {}\n"})}),"\n",(0,r.jsxs)(n.h3,{id:"the-mongodbstore",children:["The ",(0,r.jsx)(n.code,{children:"MongoDBStore"})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install @foal/mongodb\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If you use sessions with ",(0,r.jsx)(n.code,{children:"@UseSessions"}),", you must use the ",(0,r.jsx)(n.code,{children:"MongoDBStore"})," from ",(0,r.jsx)(n.code,{children:"@foal/mongodb"}),". ",(0,r.jsxs)(n.strong,{children:["The ",(0,r.jsx)(n.code,{children:"TypeORMStore"})," does not work with noSQL databases."]})]}),"\n",(0,r.jsx)(n.h2,{id:"limitations",children:"Limitations"}),"\n",(0,r.jsx)(n.p,{children:"When using MongoDB, there are some features that are not available:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["the ",(0,r.jsx)(n.code,{children:"npx foal g rest-api "})," command,"]}),"\n",(0,r.jsxs)(n.li,{children:["and the ",(0,r.jsx)(n.em,{children:"Groups & Permissions"})," system."]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(m,{...e})}):m(e)}},19365:(e,n,t)=>{t.d(n,{A:()=>s});t(96540);var r=t(34164);const a={tabItem:"tabItem_Ymn6"};var o=t(74848);function s(e){let{children:n,hidden:t,className:s}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,r.A)(a.tabItem,s),hidden:t,children:n})}},11470:(e,n,t)=>{t.d(n,{A:()=>w});var r=t(96540),a=t(34164),o=t(23104),s=t(56347),l=t(205),i=t(57485),c=t(31682),d=t(89466);function u(e){return r.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function m(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:t,attributes:r,default:a}}=e;return{value:n,label:t,attributes:r,default:a}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function h(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function p(e){let{queryString:n=!1,groupId:t}=e;const a=(0,s.W6)(),o=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,i.aZ)(o),(0,r.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(a.location.search);n.set(o,e),a.replace({...a.location,search:n.toString()})}),[o,a])]}function b(e){const{defaultValue:n,queryString:t=!1,groupId:a}=e,o=m(e),[s,i]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!h({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:o}))),[c,u]=p({queryString:t,groupId:a}),[b,f]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[a,o]=(0,d.Dv)(t);return[a,(0,r.useCallback)((e=>{t&&o.set(e)}),[t,o])]}({groupId:a}),g=(()=>{const e=c??b;return h({value:e,tabValues:o})?e:null})();(0,l.A)((()=>{g&&i(g)}),[g]);return{selectedValue:s,selectValue:(0,r.useCallback)((e=>{if(!h({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);i(e),u(e),f(e)}),[u,f,o]),tabValues:o}}var f=t(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=t(74848);function x(e){let{className:n,block:t,selectedValue:r,selectValue:s,tabValues:l}=e;const i=[],{blockElementScrollPositionUntilNextRender:c}=(0,o.a_)(),d=e=>{const n=e.currentTarget,t=i.indexOf(n),a=l[t].value;a!==r&&(c(n),s(a))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=i.indexOf(e.currentTarget)+1;n=i[t]??i[0];break}case"ArrowLeft":{const t=i.indexOf(e.currentTarget)-1;n=i[t]??i[i.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":t},n),children:l.map((e=>{let{value:n,label:t,attributes:o}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:r===n?0:-1,"aria-selected":r===n,ref:e=>i.push(e),onKeyDown:u,onClick:d,...o,className:(0,a.A)("tabs__item",g.tabItem,o?.className,{"tabs__item--active":r===n}),children:t??n},n)}))})}function v(e){let{lazy:n,children:t,selectedValue:a}=e;const o=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===a));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==a})))})}function y(e){const n=b(e);return(0,j.jsxs)("div",{className:(0,a.A)("tabs-container",g.tabList),children:[(0,j.jsx)(x,{...e,...n}),(0,j.jsx)(v,{...e,...n})]})}function w(e){const n=(0,f.A)();return(0,j.jsx)(y,{...e,children:u(e.children)},String(n))}},28453:(e,n,t)=>{t.d(n,{R:()=>s,x:()=>l});var r=t(96540);const a={},o=r.createContext(a);function s(e){const n=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/43a4db71.1508d4b1.js b/assets/js/43a4db71.1508d4b1.js new file mode 100644 index 0000000000..e46d3f2e6b --- /dev/null +++ b/assets/js/43a4db71.1508d4b1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9717],{19805:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>d,frontMatter:()=>i,metadata:()=>a,toc:()=>l});var s=n(74848),r=n(28453);const i={title:"Rate Limiting"},o=void 0,a={id:"security/rate-limiting",title:"Rate Limiting",description:"To avoid brute force attacks or overloading your application, you can set up a rate limiter to limit the number of requests a user can send to your application.",source:"@site/docs/security/rate-limiting.md",sourceDirName:"security",slug:"/security/rate-limiting",permalink:"/docs/security/rate-limiting",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/security/rate-limiting.md",tags:[],version:"current",frontMatter:{title:"Rate Limiting"},sidebar:"someSidebar",previous:{title:"CORS",permalink:"/docs/security/cors"},next:{title:"Body Size Limiting",permalink:"/docs/security/body-size-limiting"}},c={},l=[{value:"Basic Example",id:"basic-example",level:2},{value:"Usage with CORS",id:"usage-with-cors",level:2}];function p(e){const t={a:"a",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.p,{children:"To avoid brute force attacks or overloading your application, you can set up a rate limiter to limit the number of requests a user can send to your application."}),"\n",(0,s.jsx)(t.h2,{id:"basic-example",children:"Basic Example"}),"\n",(0,s.jsxs)(t.p,{children:["Foal does not provide a built-in utility for this, but you can use the ",(0,s.jsx)(t.a,{href:"https://github.com/nfriedly/express-rate-limit",children:"express-rate-limit"})," package to handle it."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{children:"npm install express express-rate-limit\n"})}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.em,{children:"src/index.ts"})}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// 3p\nimport { Config, createApp, Logger, ServiceManager } from '@foal/core';\nimport * as express from 'express';\nimport * as rateLimit from 'express-rate-limit';\n\n// App\nimport { AppController } from './app/app.controller';\n\nasync function main() {\n const expressApp = express();\n expressApp.use(rateLimit({\n // Limit each IP to 100 requests per windowMs\n max: 100,\n // 15 minutes\n windowMs: 15 * 60 * 1000,\n handler (req, res, next) {\n // Set default FoalTS headers to the response of limited requests\n res.removeHeader('X-Powered-By');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n res.setHeader('X-XSS-Protection', '1; mode=block');\n res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n \n // Send the response with the default statusCode and message from rateLimit\n res.status(this.statusCode || 429).send(this.message);\n }\n }));\n\n const serviceManager = new ServiceManager();\n const logger = serviceManager.get(Logger);\n \n const app = await createApp(AppController, { expressInstance: expressApp });\n\n const port = Config.get('port', 'number', 3001);\n app.listen(port, () => logger.info(`Listening on port ${port}...`));\n}\n\nmain()\n .catch(err => { console.error(err.stack); process.exit(1); });\n"})}),"\n",(0,s.jsx)(t.h2,{id:"usage-with-cors",children:"Usage with CORS"}),"\n",(0,s.jsxs)(t.p,{children:["In case your application needs to allow CORS requests, you can also update your ",(0,s.jsx)(t.code,{children:"index.ts"})," as follows."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"expressApp.use(rateLimit({\n max: 100,\n windowMs: 15 * 60 * 1000,\n handler (req, res, next) {\n res.removeHeader('X-Powered-By');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n res.setHeader('X-XSS-Protection', '1; mode=block');\n res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n\n // Set CORS headers\n res.setHeader('Access-Control-Allow-Origin', '*');\n if (req.method === 'OPTIONS') {\n // You may want to allow other headers depending on what you need (Authorization, etc).\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n res.setHeader('Access-Control-Allow-Methods', 'HEAD, GET, POST, PUT, PATCH, DELETE');\n }\n\n res.status(this.statusCode || 429).send(this.message);\n }\n}));\n"})})]})}function d(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>o,x:()=>a});var s=n(96540);const r={},i=s.createContext(r);function o(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/43a4db71.c7435767.js b/assets/js/43a4db71.c7435767.js deleted file mode 100644 index 8393d8e362..0000000000 --- a/assets/js/43a4db71.c7435767.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9717],{19805:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>d,frontMatter:()=>i,metadata:()=>a,toc:()=>p});var s=n(74848),r=n(28453);const i={title:"Rate Limiting"},o=void 0,a={id:"security/rate-limiting",title:"Rate Limiting",description:"To avoid brute force attacks or overloading your application, you can set up a rate limiter to limit the number of requests a user can send to your application.",source:"@site/docs/security/rate-limiting.md",sourceDirName:"security",slug:"/security/rate-limiting",permalink:"/docs/security/rate-limiting",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/security/rate-limiting.md",tags:[],version:"current",frontMatter:{title:"Rate Limiting"},sidebar:"someSidebar",previous:{title:"CORS",permalink:"/docs/security/cors"},next:{title:"Body Size Limiting",permalink:"/docs/security/body-size-limiting"}},c={},p=[{value:"Basic Example",id:"basic-example",level:2},{value:"Usage with CORS",id:"usage-with-cors",level:2}];function l(e){const t={a:"a",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.p,{children:"To avoid brute force attacks or overloading your application, you can set up a rate limiter to limit the number of requests a user can send to your application."}),"\n",(0,s.jsx)(t.h2,{id:"basic-example",children:"Basic Example"}),"\n",(0,s.jsxs)(t.p,{children:["Foal does not provide a built-in utility for this, but you can use the ",(0,s.jsx)(t.a,{href:"https://github.com/nfriedly/express-rate-limit",children:"express-rate-limit"})," package to handle it."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{children:"npm install express express-rate-limit\n"})}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.em,{children:"src/index.ts"})}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// 3p\nimport { Config, createApp, displayServerURL } from '@foal/core';\nimport * as express from 'express';\nimport * as rateLimit from 'express-rate-limit';\n\n// App\nimport { AppController } from './app/app.controller';\n\nasync function main() {\n const expressApp = express();\n expressApp.use(rateLimit({\n // Limit each IP to 100 requests per windowMs\n max: 100,\n // 15 minutes\n windowMs: 15 * 60 * 1000,\n handler (req, res, next) {\n // Set default FoalTS headers to the response of limited requests\n res.removeHeader('X-Powered-By');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n res.setHeader('X-XSS-Protection', '1; mode=block');\n res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n \n // Send the response with the default statusCode and message from rateLimit\n res.status(this.statusCode || 429).send(this.message);\n }\n }));\n \n const app = await createApp(AppController, { expressInstance: expressApp });\n\n const port = Config.get('port', 'number', 3001);\n app.listen(port, () => displayServerURL(port));\n}\n\nmain()\n .catch(err => { console.error(err.stack); process.exit(1); });\n"})}),"\n",(0,s.jsx)(t.h2,{id:"usage-with-cors",children:"Usage with CORS"}),"\n",(0,s.jsxs)(t.p,{children:["In case your application needs to allow CORS requests, you can also update your ",(0,s.jsx)(t.code,{children:"index.ts"})," as follows."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"expressApp.use(rateLimit({\n max: 100,\n windowMs: 15 * 60 * 1000,\n handler (req, res, next) {\n res.removeHeader('X-Powered-By');\n res.setHeader('X-Content-Type-Options', 'nosniff');\n res.setHeader('X-Frame-Options', 'SAMEORIGIN');\n res.setHeader('X-XSS-Protection', '1; mode=block');\n res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');\n\n // Set CORS headers\n res.setHeader('Access-Control-Allow-Origin', '*');\n if (req.method === 'OPTIONS') {\n // You may want to allow other headers depending on what you need (Authorization, etc).\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n res.setHeader('Access-Control-Allow-Methods', 'HEAD, GET, POST, PUT, PATCH, DELETE');\n }\n\n res.status(this.statusCode || 429).send(this.message);\n }\n}));\n"})})]})}function d(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>o,x:()=>a});var s=n(96540);const r={},i=s.createContext(r);function o(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/45dbd969.21c4f4fa.js b/assets/js/45dbd969.21c4f4fa.js new file mode 100644 index 0000000000..d794e92aee --- /dev/null +++ b/assets/js/45dbd969.21c4f4fa.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1682],{90509:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>a,frontMatter:()=>o,metadata:()=>d,toc:()=>i});var t=s(74848),r=s(28453);const o={title:"Controllers"},c=void 0,d={id:"architecture/controllers",title:"Controllers",description:"Description",source:"@site/docs/architecture/controllers.md",sourceDirName:"architecture",slug:"/architecture/controllers",permalink:"/docs/architecture/controllers",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/controllers.md",tags:[],version:"current",frontMatter:{title:"Controllers"},sidebar:"someSidebar",previous:{title:"Architecture Overview",permalink:"/docs/architecture/architecture-overview"},next:{title:"Services & Dependency Injection",permalink:"/docs/architecture/services-and-dependency-injection"}},l={},i=[{value:"Description",id:"description",level:2},{value:"Controller Architecture",id:"controller-architecture",level:2},{value:"The AppController",id:"the-appcontroller",level:2},{value:"Contexts & HTTP Requests",id:"contexts--http-requests",level:2},{value:"The Context object",id:"the-context-object",level:3},{value:"HTTP Requests",id:"http-requests",level:3},{value:"Read the Body",id:"read-the-body",level:4},{value:"Read Path Parameters",id:"read-path-parameters",level:4},{value:"Read Query Parameters",id:"read-query-parameters",level:4},{value:"Read Headers",id:"read-headers",level:4},{value:"Read Cookies",id:"read-cookies",level:4},{value:"The Controller Method Arguments",id:"the-controller-method-arguments",level:4},{value:"HTTP Responses",id:"http-responses",level:2},{value:"Adding Headers",id:"adding-headers",level:3},{value:"Adding Cookies",id:"adding-cookies",level:3},{value:"Testing Controllers",id:"testing-controllers",level:2},{value:"Inheriting Controllers",id:"inheriting-controllers",level:2}];function h(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"npx foal generate controller my-controller\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK } from '@foal/core';\n\nexport class ProductController {\n\n @Get('/products')\n listProducts(ctx: Context) {\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"description",children:"Description"}),"\n",(0,t.jsx)(n.p,{children:"Controllers are the front door of your application. They intercept all incoming requests and return the responses to the client."}),"\n",(0,t.jsx)(n.p,{children:"The code of a controller should be concise. If necessary, controllers can delegate some tasks to services (usually the business logic)."}),"\n",(0,t.jsx)(n.h2,{id:"controller-architecture",children:"Controller Architecture"}),"\n",(0,t.jsxs)(n.p,{children:["A controller is simply a class of which some methods are responsible for a route. These methods must be decorated by one of theses decorators ",(0,t.jsx)(n.code,{children:"Get"}),", ",(0,t.jsx)(n.code,{children:"Post"}),", ",(0,t.jsx)(n.code,{children:"Patch"}),", ",(0,t.jsx)(n.code,{children:"Put"}),", ",(0,t.jsx)(n.code,{children:"Delete"}),", ",(0,t.jsx)(n.code,{children:"Head"})," or ",(0,t.jsx)(n.code,{children:"Options"}),". They may be asynchronous."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, Post } from '@foal/core';\n\nclass MyController {\n @Get('/foo')\n foo() {\n return new HttpResponseOK('I\\'m listening to GET /foo requests.');\n }\n\n @Post('/bar')\n bar() {\n return new HttpResponseOK('I\\'m listening to POST /bar requests.');\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Controllers may have sub-controllers declared in the ",(0,t.jsx)(n.code,{children:"subControllers"})," property."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, Get, HttpResponseOK, Post } from '@foal/core';\n\nclass MySubController {\n @Get('/foo')\n foo() {\n return new HttpResponseOK('I\\'m listening to GET /barfoo/foo requests.');\n }\n}\n\nclass MyController {\n subControllers = [\n controller('/barfoo', MySubController)\n ]\n\n @Post('/bar')\n bar() {\n return new HttpResponseOK('I\\'m listening to POST /bar requests.');\n }\n}\n"})}),"\n",(0,t.jsxs)(n.h2,{id:"the-appcontroller",children:["The ",(0,t.jsx)(n.code,{children:"AppController"})]}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"AppController"})," is the main controller of your application. It is directly bound to the request handler. Every controller must be, directly or indirectly, a sub-controller of the ",(0,t.jsx)(n.code,{children:"AppController"}),"."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController } from './controllers/api.controller';\n\nexport class AppController implements IAppController {\n subControllers = [\n controller('/api', ApiController)\n ];\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"contexts--http-requests",children:"Contexts & HTTP Requests"}),"\n",(0,t.jsxs)(n.h3,{id:"the-context-object",children:["The ",(0,t.jsx)(n.code,{children:"Context"})," object"]}),"\n",(0,t.jsxs)(n.p,{children:["On every request, the controller method is called with a ",(0,t.jsx)(n.code,{children:"Context"})," object. This context is unique and specific to the request."]}),"\n",(0,t.jsx)(n.p,{children:"It has seven properties:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Name"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"request"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Request"})}),(0,t.jsx)(n.td,{children:"Gives information about the HTTP request."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"state"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"{ [key: string]: any }"})}),(0,t.jsxs)(n.td,{children:["Object which can be used to forward data accross several hooks (see ",(0,t.jsx)(n.a,{href:"/docs/architecture/hooks",children:"Hooks"}),")."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"user"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"{ [key: string]: any }"}),"|",(0,t.jsx)(n.code,{children:"null"})]}),(0,t.jsxs)(n.td,{children:["The current user (see ",(0,t.jsx)(n.a,{href:"/docs/authentication/quick-start",children:"Authentication"}),")."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"session"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"Session"}),"|",(0,t.jsx)(n.code,{children:"null"})]}),(0,t.jsx)(n.td,{children:"The session object if you use sessions."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"files"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"FileList"})}),(0,t.jsxs)(n.td,{children:["A list of file paths or buffers if you uploaded files (see ",(0,t.jsx)(n.a,{href:"/docs/common/file-storage/upload-and-download-files",children:"Upload and download files"}),")."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"controllerName"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string"})}),(0,t.jsx)(n.td,{children:"The name of the controller class."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"controllerMethodName"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string"})}),(0,t.jsx)(n.td,{children:"The name of the controller method."})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["The types of the ",(0,t.jsx)(n.code,{children:"user"})," and ",(0,t.jsx)(n.code,{children:"state"})," properties are generic. You override their types if needed:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\n\ninterface State {\n foo: string;\n}\n\ninterface User {\n id: string;\n}\n\nexport class ProductController {\n @Get('/')\n getProducts(ctx: Context) {\n // ...\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"http-requests",children:"HTTP Requests"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"request"})," property is an ",(0,t.jsx)(n.a,{href:"http://expressjs.com/",children:"ExpressJS"})," request object. Its complete documentation can be consulted ",(0,t.jsx)(n.a,{href:"http://expressjs.com/en/4x/api.html#req",children:"here"}),". The below sections detail common use cases."]}),"\n",(0,t.jsx)(n.h4,{id:"read-the-body",children:"Read the Body"}),"\n",(0,t.jsxs)(n.p,{children:["The request body is accessible with the ",(0,t.jsx)(n.code,{children:"body"})," attribute. Form data and JSON objects are automatically converted to JavaScript objects in FoalTS."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'POST /products\n\n{\n "name": "milk"\n}\n'})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseCreated, Post } from '@foal/core';\n\nclass AppController {\n @Post('/products')\n createProduct(ctx: Context) {\n const body = ctx.request.body;\n // Do something.\n return new HttpResponseCreated();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-path-parameters",children:"Read Path Parameters"}),"\n",(0,t.jsxs)(n.p,{children:["Path parameters are accessible with the ",(0,t.jsx)(n.code,{children:"params"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"GET /products/3\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/products/:id')\n readProduct(ctx: Context) {\n const productId = ctx.request.params.id;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-query-parameters",children:"Read Query Parameters"}),"\n",(0,t.jsxs)(n.p,{children:["Query parameters are accessible with the ",(0,t.jsx)(n.code,{children:"query"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"GET /products?limit=3\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/products')\n readProducts(ctx: Context) {\n const limit = ctx.request.query.limit;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-headers",children:"Read Headers"}),"\n",(0,t.jsxs)(n.p,{children:["Headers are accessible with the ",(0,t.jsx)(n.code,{children:"get"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index(ctx: Context) {\n const token = ctx.request.get('Authorization');\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-cookies",children:"Read Cookies"}),"\n",(0,t.jsxs)(n.p,{children:["Cookies are accessible through the ",(0,t.jsx)(n.code,{children:"cookies"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index(ctx: Context) {\n const sessionID: string|undefined = ctx.request.cookies.sessionID;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Signed cookies are accessible through the ",(0,t.jsx)(n.code,{children:"signedCookies"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index(ctx: Context) {\n const cookie1: string|undefined = ctx.request.signedCookies.cookie1;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["In order to use signed cookies, you must provide a secret with the configuration key ",(0,t.jsx)(n.code,{children:"settings.cookieParser.secret"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"the-controller-method-arguments",children:"The Controller Method Arguments"}),"\n",(0,t.jsx)(n.p,{children:"The path paramaters and request body are also passed as second and third arguments to the controller method."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseCreated, Put } from '@foal/core';\n\nclass AppController {\n @Put('/products/:id')\n updateProduct(ctx: Context, { id }, body) {\n // Do something.\n return new HttpResponseCreated();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"http-responses",children:"HTTP Responses"}),"\n",(0,t.jsxs)(n.p,{children:["HTTP responses are defined using ",(0,t.jsx)(n.code,{children:"HttpResponse"})," objects. Each controller method must return an instance of this class (or a ",(0,t.jsx)(n.em,{children:"promise"})," of this instance)."]}),"\n",(0,t.jsx)(n.p,{children:"Here are subclasses that you can use:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"HTTP method"}),(0,t.jsx)(n.th,{children:"Response class"}),(0,t.jsx)(n.th,{children:"Is abstract?"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"2XX Success"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"2XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseSuccess"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"200"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseOK"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"201"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseCreated"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"3XX Redirection"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"3XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseRedirection"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"301"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseMovedPermanently"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"302"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseRedirect"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"4XX Client errors"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"4XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseClientError"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"400"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseBadRequest"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"401"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseUnauthorized"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"403"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseForbidden"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"404"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseNotFound"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"405"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseMethodNotAllowed"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"409"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseConflict"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"429"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseTooManyRequests"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"5XX Server errors"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"5XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseServerError"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"500"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseInternalServerError"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"501"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseNotImplemented"})}),(0,t.jsx)(n.td,{children:"no"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Most of these responses accept a ",(0,t.jsx)(n.code,{children:"body"})," at instantiation. It can be a ",(0,t.jsx)(n.code,{children:"Buffer"})," object, a string, an object, a number, an array, or even a Node.JS stream."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a body"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"new HttpResponseBadRequest({\n message: 'The foo field is missing.'\n})\n"})}),"\n",(0,t.jsxs)(n.p,{children:["In case the body parameter is a stream, you must specify it using the ",(0,t.jsx)(n.code,{children:"stream"})," option."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a Node.JS stream as body"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"new HttpResponseOK(myStream, { stream: true })\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"HttpResponseServerError"})," constructor also accepts two other options: a ",(0,t.jsx)(n.code,{children:"Context"})," object and an error."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"new HttpResponseServerError({}, { error, ctx });\n"})}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The type of the ",(0,t.jsx)(n.code,{children:"body"})," may be constrained. This is useful if you wish to guarantee your endpoints return a certain data shape."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a constrained body type"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"interface Item {\n title: string\n}\n\n// OK\nnew HttpResponseOK({ title: 'foobar' })\n\n// Error\nnew HttpResponseOK('foobar')\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-headers",children:"Adding Headers"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setHeader('Cache-Control', 'max-age=604800, public');\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-cookies",children:"Adding Cookies"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with no cookie directives"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setCookie('state', 'foobar');\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with cookie directives"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setCookie('sessionID', 'xxxx', {\n domain: 'example.com',\n httpOnly: true,\n // expires: new Date(2022, 12, 12),\n maxAge: 3600,\n path: '/',\n sameSite: 'lax',\n secure: true,\n });\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"maxAge"})," cookie directive defines the number of ",(0,t.jsx)(n.strong,{children:"seconds"})," until the cookie expires."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a signed cookie."})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setCookie('cookie1', 'value1', {\n signed: true\n });\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["In order to use signed cookies, you must provide a secret with the configuration key ",(0,t.jsx)(n.code,{children:"settings.cookieParser.secret"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"testing-controllers",children:"Testing Controllers"}),"\n",(0,t.jsxs)(n.p,{children:["A controller is a simple class and so can be tested as is. Note that ",(0,t.jsx)(n.a,{href:"/docs/architecture/hooks",children:"hooks"})," are ignored upon testing."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"api.controller.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nclass ApiController {\n @Get('/users/me')\n @JWTRequired()\n getCurrentUser(ctx: Context) {\n return new HttpResponseOK(ctx.user);\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"api.controller.spec.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { strictEqual } from 'assert';\n\nimport { Context, createController, HttpResponseOK, isHttpResponseOK } from '@foal/core';\n\nimport { ApiController } from './api.controller';\n\ndescribe('ApiController', () => {\n\n it('should return the current user.', () => {\n // Instantiate the controller.\n const controller = createController(ApiController);\n\n // Create a fake user (the current user)\n const user = { name: 'Alix' };\n\n // Create a fake Context object to simulate the request.\n const ctx = new Context({}); // \"{}\" is the request body.\n ctx.user = user;\n\n // Execute the controller method and save the response.\n const response = controller.getCurrentUser(ctx);\n\n if (!isHttpResponseOK(response)) {\n throw new Error('The response should be an HttpResponseOK');\n }\n\n strictEqual(response.body, user);\n });\n\n});\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Due to the way packages are managed by npm, you should always use ",(0,t.jsx)(n.code,{children:"isHttpResponseOK(response)"})," rather than ",(0,t.jsx)(n.code,{children:"response instanceof HttpResponseOK"})," to avoid reference bugs."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"inheriting-controllers",children:"Inheriting Controllers"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, Post } from '@foal/core';\n\nabstract class ParentController {\n @Get('/foo')\n foo() {\n return new HttpResponseOK();\n }\n}\n\n\nclass ChildController extends ParentController {\n @Post('/bar')\n bar() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can also override ",(0,t.jsx)(n.code,{children:"foo"}),". If you don't add a ",(0,t.jsx)(n.code,{children:"Get"}),", ",(0,t.jsx)(n.code,{children:"Post"}),", ",(0,t.jsx)(n.code,{children:"Patch"}),", ",(0,t.jsx)(n.code,{children:"Put"}),", ",(0,t.jsx)(n.code,{children:"Delete"}),", ",(0,t.jsx)(n.code,{children:"Head"})," or ",(0,t.jsx)(n.code,{children:"Options"})," decorator then the parent path and HTTP method are used. If you don't add a hook, then the parent hooks are used. Otherwise they are all discarded."]})]})}function a(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>c,x:()=>d});var t=s(96540);const r={},o=t.createContext(r);function c(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/45dbd969.bdd818b7.js b/assets/js/45dbd969.bdd818b7.js deleted file mode 100644 index e748fbe619..0000000000 --- a/assets/js/45dbd969.bdd818b7.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1682],{90509:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>a,frontMatter:()=>o,metadata:()=>d,toc:()=>i});var t=s(74848),r=s(28453);const o={title:"Controllers"},c=void 0,d={id:"architecture/controllers",title:"Controllers",description:"Description",source:"@site/docs/architecture/controllers.md",sourceDirName:"architecture",slug:"/architecture/controllers",permalink:"/docs/architecture/controllers",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/controllers.md",tags:[],version:"current",frontMatter:{title:"Controllers"},sidebar:"someSidebar",previous:{title:"Architecture Overview",permalink:"/docs/architecture/architecture-overview"},next:{title:"Services & Dependency Injection",permalink:"/docs/architecture/services-and-dependency-injection"}},l={},i=[{value:"Description",id:"description",level:2},{value:"Controller Architecture",id:"controller-architecture",level:2},{value:"The AppController",id:"the-appcontroller",level:2},{value:"Contexts & HTTP Requests",id:"contexts--http-requests",level:2},{value:"The Context object",id:"the-context-object",level:3},{value:"HTTP Requests",id:"http-requests",level:3},{value:"Read the Body",id:"read-the-body",level:4},{value:"Read Path Parameters",id:"read-path-parameters",level:4},{value:"Read Query Parameters",id:"read-query-parameters",level:4},{value:"Read Headers",id:"read-headers",level:4},{value:"Read Cookies",id:"read-cookies",level:4},{value:"The Controller Method Arguments",id:"the-controller-method-arguments",level:4},{value:"HTTP Responses",id:"http-responses",level:2},{value:"Adding Headers",id:"adding-headers",level:3},{value:"Adding Cookies",id:"adding-cookies",level:3},{value:"Testing Controllers",id:"testing-controllers",level:2},{value:"Inheriting Controllers",id:"inheriting-controllers",level:2}];function h(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"foal generate controller my-controller\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK } from '@foal/core';\n\nexport class ProductController {\n\n @Get('/products')\n listProducts(ctx: Context) {\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"description",children:"Description"}),"\n",(0,t.jsx)(n.p,{children:"Controllers are the front door of your application. They intercept all incoming requests and return the responses to the client."}),"\n",(0,t.jsx)(n.p,{children:"The code of a controller should be concise. If necessary, controllers can delegate some tasks to services (usually the business logic)."}),"\n",(0,t.jsx)(n.h2,{id:"controller-architecture",children:"Controller Architecture"}),"\n",(0,t.jsxs)(n.p,{children:["A controller is simply a class of which some methods are responsible for a route. These methods must be decorated by one of theses decorators ",(0,t.jsx)(n.code,{children:"Get"}),", ",(0,t.jsx)(n.code,{children:"Post"}),", ",(0,t.jsx)(n.code,{children:"Patch"}),", ",(0,t.jsx)(n.code,{children:"Put"}),", ",(0,t.jsx)(n.code,{children:"Delete"}),", ",(0,t.jsx)(n.code,{children:"Head"})," or ",(0,t.jsx)(n.code,{children:"Options"}),". They may be asynchronous."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, Post } from '@foal/core';\n\nclass MyController {\n @Get('/foo')\n foo() {\n return new HttpResponseOK('I\\'m listening to GET /foo requests.');\n }\n\n @Post('/bar')\n bar() {\n return new HttpResponseOK('I\\'m listening to POST /bar requests.');\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Controllers may have sub-controllers declared in the ",(0,t.jsx)(n.code,{children:"subControllers"})," property."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, Get, HttpResponseOK, Post } from '@foal/core';\n\nclass MySubController {\n @Get('/foo')\n foo() {\n return new HttpResponseOK('I\\'m listening to GET /barfoo/foo requests.');\n }\n}\n\nclass MyController {\n subControllers = [\n controller('/barfoo', MySubController)\n ]\n\n @Post('/bar')\n bar() {\n return new HttpResponseOK('I\\'m listening to POST /bar requests.');\n }\n}\n"})}),"\n",(0,t.jsxs)(n.h2,{id:"the-appcontroller",children:["The ",(0,t.jsx)(n.code,{children:"AppController"})]}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"AppController"})," is the main controller of your application. It is directly bound to the request handler. Every controller must be, directly or indirectly, a sub-controller of the ",(0,t.jsx)(n.code,{children:"AppController"}),"."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController } from './controllers/api.controller';\n\nexport class AppController implements IAppController {\n subControllers = [\n controller('/api', ApiController)\n ];\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"contexts--http-requests",children:"Contexts & HTTP Requests"}),"\n",(0,t.jsxs)(n.h3,{id:"the-context-object",children:["The ",(0,t.jsx)(n.code,{children:"Context"})," object"]}),"\n",(0,t.jsxs)(n.p,{children:["On every request, the controller method is called with a ",(0,t.jsx)(n.code,{children:"Context"})," object. This context is unique and specific to the request."]}),"\n",(0,t.jsx)(n.p,{children:"It has seven properties:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Name"}),(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"request"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Request"})}),(0,t.jsx)(n.td,{children:"Gives information about the HTTP request."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"state"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"{ [key: string]: any }"})}),(0,t.jsxs)(n.td,{children:["Object which can be used to forward data accross several hooks (see ",(0,t.jsx)(n.a,{href:"/docs/architecture/hooks",children:"Hooks"}),")."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"user"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"{ [key: string]: any }"}),"|",(0,t.jsx)(n.code,{children:"null"})]}),(0,t.jsxs)(n.td,{children:["The current user (see ",(0,t.jsx)(n.a,{href:"/docs/authentication/quick-start",children:"Authentication"}),")."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"session"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"Session"}),"|",(0,t.jsx)(n.code,{children:"null"})]}),(0,t.jsx)(n.td,{children:"The session object if you use sessions."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"files"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"FileList"})}),(0,t.jsxs)(n.td,{children:["A list of file paths or buffers if you uploaded files (see ",(0,t.jsx)(n.a,{href:"/docs/common/file-storage/upload-and-download-files",children:"Upload and download files"}),")."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"controllerName"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string"})}),(0,t.jsx)(n.td,{children:"The name of the controller class."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"controllerMethodName"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"string"})}),(0,t.jsx)(n.td,{children:"The name of the controller method."})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["The types of the ",(0,t.jsx)(n.code,{children:"user"})," and ",(0,t.jsx)(n.code,{children:"state"})," properties are generic. You override their types if needed:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\n\ninterface State {\n foo: string;\n}\n\ninterface User {\n id: string;\n}\n\nexport class ProductController {\n @Get('/')\n getProducts(ctx: Context) {\n // ...\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"http-requests",children:"HTTP Requests"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"request"})," property is an ",(0,t.jsx)(n.a,{href:"http://expressjs.com/",children:"ExpressJS"})," request object. Its complete documentation can be consulted ",(0,t.jsx)(n.a,{href:"http://expressjs.com/en/4x/api.html#req",children:"here"}),". The below sections detail common use cases."]}),"\n",(0,t.jsx)(n.h4,{id:"read-the-body",children:"Read the Body"}),"\n",(0,t.jsxs)(n.p,{children:["The request body is accessible with the ",(0,t.jsx)(n.code,{children:"body"})," attribute. Form data and JSON objects are automatically converted to JavaScript objects in FoalTS."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'POST /products\n\n{\n "name": "milk"\n}\n'})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseCreated, Post } from '@foal/core';\n\nclass AppController {\n @Post('/products')\n createProduct(ctx: Context) {\n const body = ctx.request.body;\n // Do something.\n return new HttpResponseCreated();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-path-parameters",children:"Read Path Parameters"}),"\n",(0,t.jsxs)(n.p,{children:["Path parameters are accessible with the ",(0,t.jsx)(n.code,{children:"params"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"GET /products/3\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/products/:id')\n readProduct(ctx: Context) {\n const productId = ctx.request.params.id;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-query-parameters",children:"Read Query Parameters"}),"\n",(0,t.jsxs)(n.p,{children:["Query parameters are accessible with the ",(0,t.jsx)(n.code,{children:"query"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"GET /products?limit=3\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/products')\n readProducts(ctx: Context) {\n const limit = ctx.request.query.limit;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-headers",children:"Read Headers"}),"\n",(0,t.jsxs)(n.p,{children:["Headers are accessible with the ",(0,t.jsx)(n.code,{children:"get"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index(ctx: Context) {\n const token = ctx.request.get('Authorization');\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"read-cookies",children:"Read Cookies"}),"\n",(0,t.jsxs)(n.p,{children:["Cookies are accessible through the ",(0,t.jsx)(n.code,{children:"cookies"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index(ctx: Context) {\n const sessionID: string|undefined = ctx.request.cookies.sessionID;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Signed cookies are accessible through the ",(0,t.jsx)(n.code,{children:"signedCookies"})," attribute."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseOK, Get } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index(ctx: Context) {\n const cookie1: string|undefined = ctx.request.signedCookies.cookie1;\n // Do something.\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["In order to use signed cookies, you must provide a secret with the configuration key ",(0,t.jsx)(n.code,{children:"settings.cookieParser.secret"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"the-controller-method-arguments",children:"The Controller Method Arguments"}),"\n",(0,t.jsx)(n.p,{children:"The path paramaters and request body are also passed as second and third arguments to the controller method."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseCreated, Put } from '@foal/core';\n\nclass AppController {\n @Put('/products/:id')\n updateProduct(ctx: Context, { id }, body) {\n // Do something.\n return new HttpResponseCreated();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"http-responses",children:"HTTP Responses"}),"\n",(0,t.jsxs)(n.p,{children:["HTTP responses are defined using ",(0,t.jsx)(n.code,{children:"HttpResponse"})," objects. Each controller method must return an instance of this class (or a ",(0,t.jsx)(n.em,{children:"promise"})," of this instance)."]}),"\n",(0,t.jsx)(n.p,{children:"Here are subclasses that you can use:"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"HTTP method"}),(0,t.jsx)(n.th,{children:"Response class"}),(0,t.jsx)(n.th,{children:"Is abstract?"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"2XX Success"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"2XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseSuccess"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"200"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseOK"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"201"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseCreated"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"3XX Redirection"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"3XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseRedirection"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"301"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseMovedPermanently"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"302"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseRedirect"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"4XX Client errors"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"4XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseClientError"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"400"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseBadRequest"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"401"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseUnauthorized"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"403"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseForbidden"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"404"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseNotFound"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"405"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseMethodNotAllowed"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"409"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseConflict"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"429"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseTooManyRequests"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.strong,{children:"5XX Server errors"})}),(0,t.jsx)(n.td,{})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"5XX"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseServerError"})}),(0,t.jsx)(n.td,{children:"yes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"500"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseInternalServerError"})}),(0,t.jsx)(n.td,{children:"no"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"501"}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"HttpResponseNotImplemented"})}),(0,t.jsx)(n.td,{children:"no"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Most of these responses accept a ",(0,t.jsx)(n.code,{children:"body"})," at instantiation. It can be a ",(0,t.jsx)(n.code,{children:"Buffer"})," object, a string, an object, a number, an array, or even a Node.JS stream."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a body"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"new HttpResponseBadRequest({\n message: 'The foo field is missing.'\n})\n"})}),"\n",(0,t.jsxs)(n.p,{children:["In case the body parameter is a stream, you must specify it using the ",(0,t.jsx)(n.code,{children:"stream"})," option."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a Node.JS stream as body"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"new HttpResponseOK(myStream, { stream: true })\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"HttpResponseServerError"})," constructor also accepts two other options: a ",(0,t.jsx)(n.code,{children:"Context"})," object and an error."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"new HttpResponseServerError({}, { error, ctx });\n"})}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The type of the ",(0,t.jsx)(n.code,{children:"body"})," may be constrained. This is useful if you wish to guarantee your endpoints return a certain data shape."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a constrained body type"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"interface Item {\n title: string\n}\n\n// OK\nnew HttpResponseOK({ title: 'foobar' })\n\n// Error\nnew HttpResponseOK('foobar')\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-headers",children:"Adding Headers"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setHeader('Cache-Control', 'max-age=604800, public');\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-cookies",children:"Adding Cookies"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with no cookie directives"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setCookie('state', 'foobar');\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with cookie directives"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setCookie('sessionID', 'xxxx', {\n domain: 'example.com',\n httpOnly: true,\n // expires: new Date(2022, 12, 12),\n maxAge: 3600,\n path: '/',\n sameSite: 'lax',\n secure: true,\n });\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"maxAge"})," cookie directive defines the number of ",(0,t.jsx)(n.strong,{children:"seconds"})," until the cookie expires."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example with a signed cookie."})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\n\nclass AppController {\n @Get('/')\n index() {\n return new HttpResponseOK()\n .setCookie('cookie1', 'value1', {\n signed: true\n });\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["In order to use signed cookies, you must provide a secret with the configuration key ",(0,t.jsx)(n.code,{children:"settings.cookieParser.secret"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"testing-controllers",children:"Testing Controllers"}),"\n",(0,t.jsxs)(n.p,{children:["A controller is a simple class and so can be tested as is. Note that ",(0,t.jsx)(n.a,{href:"/docs/architecture/hooks",children:"hooks"})," are ignored upon testing."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"api.controller.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nclass ApiController {\n @Get('/users/me')\n @JWTRequired()\n getCurrentUser(ctx: Context) {\n return new HttpResponseOK(ctx.user);\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"api.controller.spec.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { strictEqual } from 'assert';\n\nimport { Context, createController, HttpResponseOK, isHttpResponseOK } from '@foal/core';\n\nimport { ApiController } from './api.controller';\n\ndescribe('ApiController', () => {\n\n it('should return the current user.', () => {\n // Instantiate the controller.\n const controller = createController(ApiController);\n\n // Create a fake user (the current user)\n const user = { name: 'Alix' };\n\n // Create a fake Context object to simulate the request.\n const ctx = new Context({}); // \"{}\" is the request body.\n ctx.user = user;\n\n // Execute the controller method and save the response.\n const response = controller.getCurrentUser(ctx);\n\n if (!isHttpResponseOK(response)) {\n throw new Error('The response should be an HttpResponseOK');\n }\n\n strictEqual(response.body, user);\n });\n\n});\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Due to the way packages are managed by npm, you should always use ",(0,t.jsx)(n.code,{children:"isHttpResponseOK(response)"})," rather than ",(0,t.jsx)(n.code,{children:"response instanceof HttpResponseOK"})," to avoid reference bugs."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"inheriting-controllers",children:"Inheriting Controllers"}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, Post } from '@foal/core';\n\nabstract class ParentController {\n @Get('/foo')\n foo() {\n return new HttpResponseOK();\n }\n}\n\n\nclass ChildController extends ParentController {\n @Post('/bar')\n bar() {\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["You can also override ",(0,t.jsx)(n.code,{children:"foo"}),". If you don't add a ",(0,t.jsx)(n.code,{children:"Get"}),", ",(0,t.jsx)(n.code,{children:"Post"}),", ",(0,t.jsx)(n.code,{children:"Patch"}),", ",(0,t.jsx)(n.code,{children:"Put"}),", ",(0,t.jsx)(n.code,{children:"Delete"}),", ",(0,t.jsx)(n.code,{children:"Head"})," or ",(0,t.jsx)(n.code,{children:"Options"})," decorator then the parent path and HTTP method are used. If you don't add a hook, then the parent hooks are used. Otherwise they are all discarded."]})]})}function a(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>c,x:()=>d});var t=s(96540);const r={},o=t.createContext(r);function c(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4aba3dc4.20600c54.js b/assets/js/4aba3dc4.20600c54.js new file mode 100644 index 0000000000..3f0680dc1f --- /dev/null +++ b/assets/js/4aba3dc4.20600c54.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5290],{39049:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>s,metadata:()=>a,toc:()=>d});var n=r(74848),o=r(28453);const s={title:"Your First Route",id:"tuto-5-our-first-route",slug:"5-our-first-route"},i=void 0,a={id:"tutorials/real-world-example-with-react/tuto-5-our-first-route",title:"Your First Route",description:"The database is now filled with some stories. Let's implement the first route to retrieve them.",source:"@site/docs/tutorials/real-world-example-with-react/5-our-first-route.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/5-our-first-route",permalink:"/docs/tutorials/real-world-example-with-react/5-our-first-route",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/5-our-first-route.md",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"Your First Route",id:"tuto-5-our-first-route",slug:"5-our-first-route"},sidebar:"someSidebar",previous:{title:"The Shell Scripts",permalink:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts"},next:{title:"API Testing with Swagger",permalink:"/docs/tutorials/real-world-example-with-react/6-swagger-interface"}},l={},d=[];function c(e){const t={code:"code",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:"The database is now filled with some stories. Let's implement the first route to retrieve them."}),"\n",(0,n.jsxs)(t.table,{children:[(0,n.jsx)(t.thead,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.th,{children:"API endpoint"}),(0,n.jsx)(t.th,{children:"Method"}),(0,n.jsx)(t.th,{children:"Description"})]})}),(0,n.jsx)(t.tbody,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:(0,n.jsx)(t.code,{children:"/api/stories"})}),(0,n.jsx)(t.td,{children:(0,n.jsx)(t.code,{children:"GET"})}),(0,n.jsxs)(t.td,{children:["Lists the stories of all users. An optional query parameter ",(0,n.jsx)(t.code,{children:"authorId"})," can be provided to filter the stories to be returned."]})]})})]}),"\n",(0,n.jsx)(t.p,{children:"First, generate the story controller."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"npx foal generate controller api/stories --register\n"})}),"\n",(0,n.jsxs)(t.p,{children:["A new file appears in the ",(0,n.jsx)(t.code,{children:"api"})," subdirectory. Open it and replace its contents."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { Context, Delete, Get, HttpResponseCreated, HttpResponseNoContent, HttpResponseNotFound, HttpResponseOK, Post, UserRequired, ValidateBody, ValidatePathParam, ValidateQueryParam } from '@foal/core';\nimport { Story, User } from '../../entities';\n\nexport class StoriesController {\n @Get()\n @ValidateQueryParam('authorId', { type: 'number' }, { required: false })\n async readStories(ctx: Context) {\n const authorId = ctx.request.query.authorId as number|undefined;\n\n let queryBuilder = Story\n .createQueryBuilder('story')\n .leftJoinAndSelect('story.author', 'author')\n .select([\n 'story.id',\n 'story.title',\n 'story.link',\n 'author.id',\n 'author.name'\n ]);\n\n if (authorId !== undefined) {\n queryBuilder = queryBuilder.where('author.id = :authorId', { authorId });\n }\n\n const stories = await queryBuilder.getMany();\n\n return new HttpResponseOK(stories);\n }\n}\n\n"})}),"\n",(0,n.jsxs)(t.p,{children:["The ",(0,n.jsx)(t.code,{children:"readStories"})," method retrieves and returns the stories along with some information about their authors."]}),"\n",(0,n.jsx)(t.p,{children:"When requesting this endpoint, the response body will look like this:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-json",children:'[\n {\n "id": 1,\n "title": "How to build a simple to-do list",\n "link": "https://foalts.org/docs/tutorials/simple-todo-list/1-installation",\n "author": {\n "id": 1,\n "name": "John"\n }\n },\n {\n "id": 2,\n "title": "FoalTS architecture overview",\n "link": "https://foalts.org/docs/architecture/architecture-overview",\n "author": {\n "id": 2,\n "name": "Mary"\n }\n },\n {\n "id": 3,\n "title": "Authentication with Foal",\n "link": "https://foalts.org/docs/authentication/quick-start",\n "author": {\n "id": 2,\n "name": "Mary"\n }\n },\n]\n'})})]})}function u(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},28453:(e,t,r)=>{r.d(t,{R:()=>i,x:()=>a});var n=r(96540);const o={},s=n.createContext(o);function i(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4aba3dc4.dc2b0b89.js b/assets/js/4aba3dc4.dc2b0b89.js deleted file mode 100644 index 3e9f5a761a..0000000000 --- a/assets/js/4aba3dc4.dc2b0b89.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5290],{39049:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>u,frontMatter:()=>s,metadata:()=>a,toc:()=>d});var n=r(74848),o=r(28453);const s={title:"Your First Route",id:"tuto-5-our-first-route",slug:"5-our-first-route"},i=void 0,a={id:"tutorials/real-world-example-with-react/tuto-5-our-first-route",title:"Your First Route",description:"The database is now filled with some stories. Let's implement the first route to retrieve them.",source:"@site/docs/tutorials/real-world-example-with-react/5-our-first-route.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/5-our-first-route",permalink:"/docs/tutorials/real-world-example-with-react/5-our-first-route",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/5-our-first-route.md",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"Your First Route",id:"tuto-5-our-first-route",slug:"5-our-first-route"},sidebar:"someSidebar",previous:{title:"The Shell Scripts",permalink:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts"},next:{title:"API Testing with Swagger",permalink:"/docs/tutorials/real-world-example-with-react/6-swagger-interface"}},l={},d=[];function c(e){const t={code:"code",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:"The database is now filled with some stories. Let's implement the first route to retrieve them."}),"\n",(0,n.jsxs)(t.table,{children:[(0,n.jsx)(t.thead,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.th,{children:"API endpoint"}),(0,n.jsx)(t.th,{children:"Method"}),(0,n.jsx)(t.th,{children:"Description"})]})}),(0,n.jsx)(t.tbody,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:(0,n.jsx)(t.code,{children:"/api/stories"})}),(0,n.jsx)(t.td,{children:(0,n.jsx)(t.code,{children:"GET"})}),(0,n.jsxs)(t.td,{children:["Lists the stories of all users. An optional query parameter ",(0,n.jsx)(t.code,{children:"authorId"})," can be provided to filter the stories to be returned."]})]})})]}),"\n",(0,n.jsx)(t.p,{children:"First, generate the story controller."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"foal generate controller api/stories --register\n"})}),"\n",(0,n.jsxs)(t.p,{children:["A new file appears in the ",(0,n.jsx)(t.code,{children:"api"})," subdirectory. Open it and replace its contents."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { Context, Delete, Get, HttpResponseCreated, HttpResponseNoContent, HttpResponseNotFound, HttpResponseOK, Post, UserRequired, ValidateBody, ValidatePathParam, ValidateQueryParam } from '@foal/core';\nimport { Story, User } from '../../entities';\n\nexport class StoriesController {\n @Get()\n @ValidateQueryParam('authorId', { type: 'number' }, { required: false })\n async readStories(ctx: Context) {\n const authorId = ctx.request.query.authorId as number|undefined;\n\n let queryBuilder = Story\n .createQueryBuilder('story')\n .leftJoinAndSelect('story.author', 'author')\n .select([\n 'story.id',\n 'story.title',\n 'story.link',\n 'author.id',\n 'author.name'\n ]);\n\n if (authorId !== undefined) {\n queryBuilder = queryBuilder.where('author.id = :authorId', { authorId });\n }\n\n const stories = await queryBuilder.getMany();\n\n return new HttpResponseOK(stories);\n }\n}\n\n"})}),"\n",(0,n.jsxs)(t.p,{children:["The ",(0,n.jsx)(t.code,{children:"readStories"})," method retrieves and returns the stories along with some information about their authors."]}),"\n",(0,n.jsx)(t.p,{children:"When requesting this endpoint, the response body will look like this:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-json",children:'[\n {\n "id": 1,\n "title": "How to build a simple to-do list",\n "link": "https://foalts.org/docs/tutorials/simple-todo-list/1-installation",\n "author": {\n "id": 1,\n "name": "John"\n }\n },\n {\n "id": 2,\n "title": "FoalTS architecture overview",\n "link": "https://foalts.org/docs/architecture/architecture-overview",\n "author": {\n "id": 2,\n "name": "Mary"\n }\n },\n {\n "id": 3,\n "title": "Authentication with Foal",\n "link": "https://foalts.org/docs/authentication/quick-start",\n "author": {\n "id": 2,\n "name": "Mary"\n }\n },\n]\n'})})]})}function u(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},28453:(e,t,r)=>{r.d(t,{R:()=>i,x:()=>a});var n=r(96540);const o={},s=n.createContext(o);function i(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/519173bc.66a01c06.js b/assets/js/519173bc.66a01c06.js new file mode 100644 index 0000000000..e102a763db --- /dev/null +++ b/assets/js/519173bc.66a01c06.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3548],{23451:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>i,default:()=>g,frontMatter:()=>r,metadata:()=>l,toc:()=>d});var s=o(74848),t=o(28453);const r={title:"Logging"},i=void 0,l={id:"common/logging",title:"Logging",description:"Foal provides an advanced built-in logger. This page shows how to use it.",source:"@site/docs/common/logging.md",sourceDirName:"common",slug:"/common/logging",permalink:"/docs/common/logging",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/logging.md",tags:[],version:"current",frontMatter:{title:"Logging"},sidebar:"someSidebar",previous:{title:"Serialization",permalink:"/docs/common/serialization"},next:{title:"Async tasks",permalink:"/docs/common/async-tasks"}},a={},d=[{value:"Recommended Configuration",id:"recommended-configuration",level:2},{value:"Accessing and Using the Logger",id:"accessing-and-using-the-logger",level:2},{value:"Levels of Logs",id:"levels-of-logs",level:2},{value:"Log Ouput Formats",id:"log-ouput-formats",level:2},{value:"The dev format",id:"the-dev-format",level:3},{value:"The raw format",id:"the-raw-format",level:3},{value:"The json format",id:"the-json-format",level:3},{value:"Hiding logs: the none format",id:"hiding-logs-the-none-format",level:3},{value:"HTTP Request Logging",id:"http-request-logging",level:2},{value:"Adding other parameters to the logs",id:"adding-other-parameters-to-the-logs",level:3},{value:"Formatting the log message (deprecated)",id:"formatting-the-log-message-deprecated",level:3},{value:"Disabling HTTP Request Logging",id:"disabling-http-request-logging",level:3},{value:"Socket.io Message Logging",id:"socketio-message-logging",level:2},{value:"Disabling Socket.io Message Logging",id:"disabling-socketio-message-logging",level:3},{value:"Error Logging",id:"error-logging",level:2},{value:"Disabling Error Logging",id:"disabling-error-logging",level:3},{value:"Log correlation (by HTTP request, user ID, etc)",id:"log-correlation-by-http-request-user-id-etc",level:2},{value:"Transports",id:"transports",level:2},{value:"Logging Hook (deprecated)",id:"logging-hook-deprecated",level:2}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Foal provides an advanced built-in logger. This page shows how to use it."}),"\n",(0,s.jsx)(n.h2,{id:"recommended-configuration",children:"Recommended Configuration"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"config/default.json"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "loggerFormat": "foal"\n }\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"config/development.json"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "format": "dev"\n }\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"accessing-and-using-the-logger",children:"Accessing and Using the Logger"}),"\n",(0,s.jsxs)(n.p,{children:["To log a message anywhere in the application, you can inject the ",(0,s.jsx)(n.code,{children:"Logger"})," service and use its ",(0,s.jsx)(n.code,{children:"info"})," method. This methods takes two parameters:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["a required ",(0,s.jsx)(n.code,{children:"message"})," string,"]}),"\n",(0,s.jsxs)(n.li,{children:["and an optional ",(0,s.jsx)(n.code,{children:"params"})," object if you wish to add additional data to the log."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example with a controller"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Logger, Post } from '@foal/core';\n\nexport class AuthController {\n @dependency\n logger: Logger;\n\n @Post('/signup')\n signup() {\n ...\n this.logger.info('Someone signed up!');\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example with a hook"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { Hook, Logger } from '@foal/core';\n\nexport function LogUserId() {\n return Hook((ctx, services) => {\n const logger = services.get(Logger);\n logger.info(`Logging user ID`, { userId: ctx.user.id });\n });\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"levels-of-logs",children:"Levels of Logs"}),"\n",(0,s.jsx)(n.p,{children:"The logger supports four levels of logs:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.code,{children:"debug"})," level which is commonly used to log debugging data,"]}),"\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.code,{children:"info"})," level which logs informative data,"]}),"\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.code,{children:"warn"})," level which logs data that requires attention,"]}),"\n",(0,s.jsxs)(n.li,{children:["and the ",(0,s.jsx)(n.code,{children:"error"})," level which logs errors."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Examples"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"this.logger.debug('This a debug message');\nthis.logger.info('This an info message');\nthis.logger.warn('This a warn message');\nthis.logger.error('This an error message');\n\nthis.logger.log('debug', 'This a debug message');\n"})}),"\n",(0,s.jsxs)(n.p,{children:["By default, only the ",(0,s.jsx)(n.code,{children:"info"}),", ",(0,s.jsx)(n.code,{children:"warn"})," and ",(0,s.jsx)(n.code,{children:"error"})," messages are logged in the console. If you wish to log all messages, you can update your configuration as follows:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "logLevel": "debug"\n }\n }\n}\n'})}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.th,{children:["Value of ",(0,s.jsx)(n.code,{children:"settings.logger.logLevel"})]}),(0,s.jsx)(n.th,{children:"Levels of logs displayed"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"debug"})}),(0,s.jsx)(n.td,{children:"error, warn, info, debug"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"info"})}),(0,s.jsx)(n.td,{children:"error, warn, info"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"warn"})}),(0,s.jsx)(n.td,{children:"error, warn"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"error"})}),(0,s.jsx)(n.td,{children:"error"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"log-ouput-formats",children:"Log Ouput Formats"}),"\n",(0,s.jsxs)(n.p,{children:["Foal's logger lets you log your messages in three different ways: ",(0,s.jsx)(n.code,{children:"raw"})," (default), ",(0,s.jsx)(n.code,{children:"dev"})," and ",(0,s.jsx)(n.code,{children:"json"}),"."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example of configuration"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "format": "json"\n }\n }\n}\n'})}),"\n",(0,s.jsxs)(n.h3,{id:"the-dev-format",children:["The ",(0,s.jsx)(n.code,{children:"dev"})," format"]}),"\n",(0,s.jsxs)(n.p,{children:["With this format, the logged output contains a small timestamp, beautiful colors and the message. The logger also displays an ",(0,s.jsx)(n.code,{children:"error"})," if one is passed as parameter and it prettifies the HTTP request logs."]}),"\n",(0,s.jsx)(n.p,{children:"This format is adapted to a development environment and focuses on reducing noise."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"dev format",src:o(64700).A+"",width:"1754",height:"260"})}),"\n",(0,s.jsxs)(n.h3,{id:"the-raw-format",children:["The ",(0,s.jsx)(n.code,{children:"raw"})," format"]}),"\n",(0,s.jsx)(n.p,{children:"This format aims to log much more information and is suitable for a production environment."}),"\n",(0,s.jsx)(n.p,{children:"The output contains a complete time stamp, the log level, the message and all parameters passed to the logger if any."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"raw format",src:o(72345).A+"",width:"1754",height:"474"})}),"\n",(0,s.jsxs)(n.h3,{id:"the-json-format",children:["The ",(0,s.jsx)(n.code,{children:"json"})," format"]}),"\n",(0,s.jsxs)(n.p,{children:["Similar to the ",(0,s.jsx)(n.code,{children:"raw"})," one, this format prints the same information except that it is displayed with a JSON. This format is useful if you need to diggest the logs with another log tool (such as an aggregator for example)."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"raw format",src:o(45349).A+"",width:"1754",height:"206"})}),"\n",(0,s.jsxs)(n.h3,{id:"hiding-logs-the-none-format",children:["Hiding logs: the ",(0,s.jsx)(n.code,{children:"none"})," format"]}),"\n",(0,s.jsxs)(n.p,{children:["If you wish to completly mask logs, you can use the ",(0,s.jsx)(n.code,{children:"none"})," format."]}),"\n",(0,s.jsx)(n.h2,{id:"http-request-logging",children:"HTTP Request Logging"}),"\n",(0,s.jsx)(n.p,{children:"Each request received by Foal is logged with the INFO level."}),"\n",(0,s.jsxs)(n.p,{children:["With the configuration key ",(0,s.jsx)(n.code,{children:"settings.loggerFormat"})," set to ",(0,s.jsx)(n.code,{children:'"foal"'}),", the messages start with ",(0,s.jsx)(n.code,{children:"HTTP request -"})," and end with the request method and URL. The log parameters include the response status code and content length as well as the response time and the request method and URL."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Note: the query parameters are not logged to avoid logging sensitive data (such as an API key)."}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"adding-other-parameters-to-the-logs",children:"Adding other parameters to the logs"}),"\n",(0,s.jsxs)(n.p,{children:["If the default logged HTTP parameters are not sufficient in your case, you can extend them with the option ",(0,s.jsx)(n.code,{children:"getHttpLogParams"})," in ",(0,s.jsx)(n.code,{children:"createApp"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { createApp, getHttpLogParamsDefault } from '@foal/core';\n\nconst app = await createApp({\n getHttpLogParams: (tokens, req, res) => ({\n ...getHttpLogParamsDefault(tokens, req, res),\n myCustomHeader: req.get('my-custom-header'),\n })\n})\n"})}),"\n",(0,s.jsx)(n.h3,{id:"formatting-the-log-message-deprecated",children:"Formatting the log message (deprecated)"}),"\n",(0,s.jsxs)(n.p,{children:["If you wish to customize the HTTP log messages, you can set the value of the ",(0,s.jsx)(n.code,{children:"loggerFormat.loggerFormat"})," configuration to a format supported by ",(0,s.jsx)(n.a,{href:"https://www.npmjs.com/package/morgan",children:"morgan"}),". With this technique, no parameters will be logged though."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "loggerFormat": "tiny"\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"disabling-http-request-logging",children:"Disabling HTTP Request Logging"}),"\n",(0,s.jsxs)(n.p,{children:["In some scenarios and environments, you might want to disable HTTP request logging. You can achieve this by setting the ",(0,s.jsx)(n.code,{children:"loggerFormat"})," configuration option to ",(0,s.jsx)(n.code,{children:"none"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "loggerFormat": "none"\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"socketio-message-logging",children:"Socket.io Message Logging"}),"\n",(0,s.jsx)(n.p,{children:"Each message, connection or disconnection is logged with the INFO level."}),"\n",(0,s.jsxs)(n.p,{children:["When a client establishes a connection, ",(0,s.jsx)(n.code,{children:"Socket.io connection"})," is logged with the socket ID as parameter."]}),"\n",(0,s.jsxs)(n.p,{children:["When a client disconnects, ",(0,s.jsx)(n.code,{children:"Socket.io disconnection"})," is logged with the socket ID and the reason of the disconnection as parameters."]}),"\n",(0,s.jsxs)(n.p,{children:["When a message is received, ",(0,s.jsx)(n.code,{children:"Socket.io message received - ${eventName}"})," is logged with the event name and the response status as parameters."]}),"\n",(0,s.jsx)(n.h3,{id:"disabling-socketio-message-logging",children:"Disabling Socket.io Message Logging"}),"\n",(0,s.jsxs)(n.p,{children:["In some scenarios and environments, you might want to disable socket.io message logging. You can achieve this by setting the ",(0,s.jsx)(n.code,{children:"settings.logger.logSocketioMessages"})," configuration option to ",(0,s.jsx)(n.code,{children:"false"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "logSocketioMessages": false\n }\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"error-logging",children:"Error Logging"}),"\n",(0,s.jsxs)(n.p,{children:["When an error is thrown (or rejected) in a hook, controller or service and is not caught, the error is logged using the ",(0,s.jsx)(n.code,{children:"Logger.error"})," method."]}),"\n",(0,s.jsx)(n.h3,{id:"disabling-error-logging",children:"Disabling Error Logging"}),"\n",(0,s.jsxs)(n.p,{children:["In some scenarios, you might want to disable error logging. You can achieve this by setting the ",(0,s.jsx)(n.code,{children:"allErrors"})," configuration option to false."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "allErrors": false\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"log-correlation-by-http-request-user-id-etc",children:"Log correlation (by HTTP request, user ID, etc)"}),"\n",(0,s.jsx)(n.p,{children:"When logs are generated in large quantities, we often like to aggregate them by request or user. This can be done using Foal's log context."}),"\n",(0,s.jsx)(n.p,{children:"When receiving an HTTP request, Foal adds the request ID to the logger context. On each subsequent call to the logger, it will behave as if the request ID had been passed as a parameter."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"export class AppController {\n @dependency\n logger: Logger;\n\n @Get('/foo')\n getFoo(ctx: Context) {\n this.logger.info('Hello world');\n // equivalent to this.logger.info('Hello world', { requestId: ctx.request.id });\n\n setTimeout(() => {\n this.logger.info('Hello world');\n // equivalent to this.logger.info('Hello world', { requestId: ctx.request.id });\n }, 1000)\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In the same way, the authentification hooks ",(0,s.jsx)(n.code,{children:"@JWTRequired"}),", ",(0,s.jsx)(n.code,{children:"@JWTOptional"})," and ",(0,s.jsx)(n.code,{children:"@UseSessions"})," will add the ",(0,s.jsx)(n.code,{children:"userId"})," (if any) to the logger context."]}),"\n",(0,s.jsx)(n.p,{children:"When using a Socket.io controller, the socket ID and message ID are also added to the logger context."}),"\n",(0,s.jsx)(n.p,{children:"This mecanism helps filter logs of a specific request or specific user in a logging tool."}),"\n",(0,s.jsx)(n.p,{children:"If needed, you call also add manually custom parameters to the logger context with this fonction:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"logger.addLogContext('myKey', 'myValue');\n"})}),"\n",(0,s.jsx)(n.h2,{id:"transports",children:"Transports"}),"\n",(0,s.jsxs)(n.p,{children:["All logs are printed using the ",(0,s.jsx)(n.code,{children:"console.log"})," function."]}),"\n",(0,s.jsx)(n.p,{children:"If you also wish to consume the logs in another way (for example, to send them to a third-party error-tracking or logging tool), you can add one or more transports to the logger:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"logger.addTransport((level: 'debug'|'warn'|'info'|'error', log: string) => {\n // Do something\n})\n"})}),"\n",(0,s.jsx)(n.h2,{id:"logging-hook-deprecated",children:"Logging Hook (deprecated)"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["This hook is deprecated and will be removed in a next release. Use the ",(0,s.jsx)(n.code,{children:"Logger"})," service in a custom hook instead."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["FoalTS provides a convenient hook for logging debug messages: ",(0,s.jsx)(n.code,{children:"Log(message: string, options: LogOptions = {})"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"interface LogOptions {\n body?: boolean;\n params?: boolean;\n headers?: string[]|boolean;\n query?: boolean;\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, Log } from '@foal/core';\n\n@Log('AppController', {\n body: true,\n headers: [ 'X-CSRF-Token' ],\n params: true,\n query: true\n})\nexport class AppController {\n @Get()\n index() {\n return new HttpResponseOK();\n }\n}\n"})})]})}function g(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},64700:(e,n,o)=>{o.d(n,{A:()=>s});const s=o.p+"assets/images/dev-format-38d7e2e0c32975ec1126097a40e983df.png"},45349:(e,n,o)=>{o.d(n,{A:()=>s});const s=o.p+"assets/images/json-format-3fd891344d40c3b23aeb178c3eb94b6e.png"},72345:(e,n,o)=>{o.d(n,{A:()=>s});const s=o.p+"assets/images/raw-format-2dccbd1406071bb3c6b3758c4f6055fb.png"},28453:(e,n,o)=>{o.d(n,{R:()=>i,x:()=>l});var s=o(96540);const t={},r=s.createContext(t);function i(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/519173bc.b47c273e.js b/assets/js/519173bc.b47c273e.js deleted file mode 100644 index 578a2425a8..0000000000 --- a/assets/js/519173bc.b47c273e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3548],{23451:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>a,contentTitle:()=>i,default:()=>g,frontMatter:()=>r,metadata:()=>l,toc:()=>d});var s=o(74848),t=o(28453);const r={title:"Logging"},i=void 0,l={id:"common/logging",title:"Logging",description:"Foal provides an advanced built-in logger. This page shows how to use it.",source:"@site/docs/common/logging.md",sourceDirName:"common",slug:"/common/logging",permalink:"/docs/common/logging",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/logging.md",tags:[],version:"current",frontMatter:{title:"Logging"},sidebar:"someSidebar",previous:{title:"Serialization",permalink:"/docs/common/serialization"},next:{title:"Task Scheduling",permalink:"/docs/common/task-scheduling"}},a={},d=[{value:"Recommended Configuration",id:"recommended-configuration",level:2},{value:"Accessing and Using the Logger",id:"accessing-and-using-the-logger",level:2},{value:"Levels of Logs",id:"levels-of-logs",level:2},{value:"Log Ouput Formats",id:"log-ouput-formats",level:2},{value:"The dev format",id:"the-dev-format",level:3},{value:"The raw format",id:"the-raw-format",level:3},{value:"The json format",id:"the-json-format",level:3},{value:"Hiding logs: the none format",id:"hiding-logs-the-none-format",level:3},{value:"HTTP Request Logging",id:"http-request-logging",level:2},{value:"Adding other parameters to the logs",id:"adding-other-parameters-to-the-logs",level:3},{value:"Formatting the log message (deprecated)",id:"formatting-the-log-message-deprecated",level:3},{value:"Disabling HTTP Request Logging",id:"disabling-http-request-logging",level:3},{value:"Socket.io Message Logging",id:"socketio-message-logging",level:2},{value:"Disabling Socket.io Message Logging",id:"disabling-socketio-message-logging",level:3},{value:"Error Logging",id:"error-logging",level:2},{value:"Disabling Error Logging",id:"disabling-error-logging",level:3},{value:"Log correlation (by HTTP request, user ID, etc)",id:"log-correlation-by-http-request-user-id-etc",level:2},{value:"Transports",id:"transports",level:2},{value:"Logging Hook (deprecated)",id:"logging-hook-deprecated",level:2}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"Foal provides an advanced built-in logger. This page shows how to use it."}),"\n",(0,s.jsx)(n.h2,{id:"recommended-configuration",children:"Recommended Configuration"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"config/default.json"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "loggerFormat": "foal"\n }\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"config/development.json"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "format": "dev"\n }\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"accessing-and-using-the-logger",children:"Accessing and Using the Logger"}),"\n",(0,s.jsxs)(n.p,{children:["To log a message anywhere in the application, you can inject the ",(0,s.jsx)(n.code,{children:"Logger"})," service and use its ",(0,s.jsx)(n.code,{children:"info"})," method. This methods takes two parameters:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["a required ",(0,s.jsx)(n.code,{children:"message"})," string,"]}),"\n",(0,s.jsxs)(n.li,{children:["and an optional ",(0,s.jsx)(n.code,{children:"params"})," object if you wish to add additional data to the log."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example with a controller"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Logger, Post } from '@foal/core';\n\nexport class AuthController {\n @dependency\n logger: Logger;\n\n @Post('/signup')\n signup() {\n ...\n this.logger.info('Someone signed up!');\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example with a hook"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { Hook, Logger } from '@foal/core';\n\nexport function LogUserId() {\n return Hook((ctx, services) => {\n const logger = services.get(Logger);\n logger.info(`Logging user ID`, { userId: ctx.user.id });\n });\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"levels-of-logs",children:"Levels of Logs"}),"\n",(0,s.jsx)(n.p,{children:"The logger supports four levels of logs:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.code,{children:"debug"})," level which is commonly used to log debugging data,"]}),"\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.code,{children:"info"})," level which logs informative data,"]}),"\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.code,{children:"warn"})," level which logs data that requires attention,"]}),"\n",(0,s.jsxs)(n.li,{children:["and the ",(0,s.jsx)(n.code,{children:"error"})," level which logs errors."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Examples"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"this.logger.debug('This a debug message');\nthis.logger.info('This an info message');\nthis.logger.warn('This a warn message');\nthis.logger.error('This an error message');\n\nthis.logger.log('debug', 'This a debug message');\n"})}),"\n",(0,s.jsxs)(n.p,{children:["By default, only the ",(0,s.jsx)(n.code,{children:"info"}),", ",(0,s.jsx)(n.code,{children:"warn"})," and ",(0,s.jsx)(n.code,{children:"error"})," messages are logged in the console. If you wish to log all messages, you can update your configuration as follows:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "logLevel": "debug"\n }\n }\n}\n'})}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.th,{children:["Value of ",(0,s.jsx)(n.code,{children:"settings.logger.logLevel"})]}),(0,s.jsx)(n.th,{children:"Levels of logs displayed"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"debug"})}),(0,s.jsx)(n.td,{children:"error, warn, info, debug"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"info"})}),(0,s.jsx)(n.td,{children:"error, warn, info"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"warn"})}),(0,s.jsx)(n.td,{children:"error, warn"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"error"})}),(0,s.jsx)(n.td,{children:"error"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"log-ouput-formats",children:"Log Ouput Formats"}),"\n",(0,s.jsxs)(n.p,{children:["Foal's logger lets you log your messages in three different ways: ",(0,s.jsx)(n.code,{children:"raw"})," (default), ",(0,s.jsx)(n.code,{children:"dev"})," and ",(0,s.jsx)(n.code,{children:"json"}),"."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example of configuration"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "format": "json"\n }\n }\n}\n'})}),"\n",(0,s.jsxs)(n.h3,{id:"the-dev-format",children:["The ",(0,s.jsx)(n.code,{children:"dev"})," format"]}),"\n",(0,s.jsxs)(n.p,{children:["With this format, the logged output contains a small timestamp, beautiful colors and the message. The logger also displays an ",(0,s.jsx)(n.code,{children:"error"})," if one is passed as parameter and it prettifies the HTTP request logs."]}),"\n",(0,s.jsx)(n.p,{children:"This format is adapted to a development environment and focuses on reducing noise."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"dev format",src:o(64700).A+"",width:"1754",height:"260"})}),"\n",(0,s.jsxs)(n.h3,{id:"the-raw-format",children:["The ",(0,s.jsx)(n.code,{children:"raw"})," format"]}),"\n",(0,s.jsx)(n.p,{children:"This format aims to log much more information and is suitable for a production environment."}),"\n",(0,s.jsx)(n.p,{children:"The output contains a complete time stamp, the log level, the message and all parameters passed to the logger if any."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"raw format",src:o(72345).A+"",width:"1754",height:"474"})}),"\n",(0,s.jsxs)(n.h3,{id:"the-json-format",children:["The ",(0,s.jsx)(n.code,{children:"json"})," format"]}),"\n",(0,s.jsxs)(n.p,{children:["Similar to the ",(0,s.jsx)(n.code,{children:"raw"})," one, this format prints the same information except that it is displayed with a JSON. This format is useful if you need to diggest the logs with another log tool (such as an aggregator for example)."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"raw format",src:o(45349).A+"",width:"1754",height:"206"})}),"\n",(0,s.jsxs)(n.h3,{id:"hiding-logs-the-none-format",children:["Hiding logs: the ",(0,s.jsx)(n.code,{children:"none"})," format"]}),"\n",(0,s.jsxs)(n.p,{children:["If you wish to completly mask logs, you can use the ",(0,s.jsx)(n.code,{children:"none"})," format."]}),"\n",(0,s.jsx)(n.h2,{id:"http-request-logging",children:"HTTP Request Logging"}),"\n",(0,s.jsx)(n.p,{children:"Each request received by Foal is logged with the INFO level."}),"\n",(0,s.jsxs)(n.p,{children:["With the configuration key ",(0,s.jsx)(n.code,{children:"settings.loggerFormat"})," set to ",(0,s.jsx)(n.code,{children:'"foal"'}),", the messages start with ",(0,s.jsx)(n.code,{children:"HTTP request -"})," and end with the request method and URL. The log parameters include the response status code and content length as well as the response time and the request method and URL."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Note: the query parameters are not logged to avoid logging sensitive data (such as an API key)."}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"adding-other-parameters-to-the-logs",children:"Adding other parameters to the logs"}),"\n",(0,s.jsxs)(n.p,{children:["If the default logged HTTP parameters are not sufficient in your case, you can extend them with the option ",(0,s.jsx)(n.code,{children:"getHttpLogParams"})," in ",(0,s.jsx)(n.code,{children:"createApp"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { createApp, getHttpLogParamsDefault } from '@foal/core';\n\nconst app = await createApp({\n getHttpLogParams: (tokens, req, res) => ({\n ...getHttpLogParamsDefault(tokens, req, res),\n myCustomHeader: req.get('my-custom-header'),\n })\n})\n"})}),"\n",(0,s.jsx)(n.h3,{id:"formatting-the-log-message-deprecated",children:"Formatting the log message (deprecated)"}),"\n",(0,s.jsxs)(n.p,{children:["If you wish to customize the HTTP log messages, you can set the value of the ",(0,s.jsx)(n.code,{children:"loggerFormat.loggerFormat"})," configuration to a format supported by ",(0,s.jsx)(n.a,{href:"https://www.npmjs.com/package/morgan",children:"morgan"}),". With this technique, no parameters will be logged though."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "loggerFormat": "tiny"\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"disabling-http-request-logging",children:"Disabling HTTP Request Logging"}),"\n",(0,s.jsxs)(n.p,{children:["In some scenarios and environments, you might want to disable HTTP request logging. You can achieve this by setting the ",(0,s.jsx)(n.code,{children:"loggerFormat"})," configuration option to ",(0,s.jsx)(n.code,{children:"none"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "loggerFormat": "none"\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"socketio-message-logging",children:"Socket.io Message Logging"}),"\n",(0,s.jsx)(n.p,{children:"Each message, connection or disconnection is logged with the INFO level."}),"\n",(0,s.jsxs)(n.p,{children:["When a client establishes a connection, ",(0,s.jsx)(n.code,{children:"Socket.io connection"})," is logged with the socket ID as parameter."]}),"\n",(0,s.jsxs)(n.p,{children:["When a client disconnects, ",(0,s.jsx)(n.code,{children:"Socket.io disconnection"})," is logged with the socket ID and the reason of the disconnection as parameters."]}),"\n",(0,s.jsxs)(n.p,{children:["When a message is received, ",(0,s.jsx)(n.code,{children:"Socket.io message received - ${eventName}"})," is logged with the event name and the response status as parameters."]}),"\n",(0,s.jsx)(n.h3,{id:"disabling-socketio-message-logging",children:"Disabling Socket.io Message Logging"}),"\n",(0,s.jsxs)(n.p,{children:["In some scenarios and environments, you might want to disable socket.io message logging. You can achieve this by setting the ",(0,s.jsx)(n.code,{children:"settings.logger.logSocketioMessages"})," configuration option to ",(0,s.jsx)(n.code,{children:"false"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "logger": {\n "logSocketioMessages": false\n }\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"error-logging",children:"Error Logging"}),"\n",(0,s.jsxs)(n.p,{children:["When an error is thrown (or rejected) in a hook, controller or service and is not caught, the error is logged using the ",(0,s.jsx)(n.code,{children:"Logger.error"})," method."]}),"\n",(0,s.jsx)(n.h3,{id:"disabling-error-logging",children:"Disabling Error Logging"}),"\n",(0,s.jsxs)(n.p,{children:["In some scenarios, you might want to disable error logging. You can achieve this by setting the ",(0,s.jsx)(n.code,{children:"allErrors"})," configuration option to false."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "allErrors": false\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"log-correlation-by-http-request-user-id-etc",children:"Log correlation (by HTTP request, user ID, etc)"}),"\n",(0,s.jsx)(n.p,{children:"When logs are generated in large quantities, we often like to aggregate them by request or user. This can be done using Foal's log context."}),"\n",(0,s.jsx)(n.p,{children:"When receiving an HTTP request, Foal adds the request ID to the logger context. On each subsequent call to the logger, it will behave as if the request ID had been passed as a parameter."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"export class AppController {\n @dependency\n logger: Logger;\n\n @Get('/foo')\n getFoo(ctx: Context) {\n this.logger.info('Hello world');\n // equivalent to this.logger.info('Hello world', { requestId: ctx.request.id });\n\n setTimeout(() => {\n this.logger.info('Hello world');\n // equivalent to this.logger.info('Hello world', { requestId: ctx.request.id });\n }, 1000)\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["In the same way, the authentification hooks ",(0,s.jsx)(n.code,{children:"@JWTRequired"}),", ",(0,s.jsx)(n.code,{children:"@JWTOptional"})," and ",(0,s.jsx)(n.code,{children:"@UseSessions"})," will add the ",(0,s.jsx)(n.code,{children:"userId"})," (if any) to the logger context."]}),"\n",(0,s.jsx)(n.p,{children:"When using a Socket.io controller, the socket ID and message ID are also added to the logger context."}),"\n",(0,s.jsx)(n.p,{children:"This mecanism helps filter logs of a specific request or specific user in a logging tool."}),"\n",(0,s.jsx)(n.p,{children:"If needed, you call also add manually custom parameters to the logger context with this fonction:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"logger.addLogContext('myKey', 'myValue');\n"})}),"\n",(0,s.jsx)(n.h2,{id:"transports",children:"Transports"}),"\n",(0,s.jsxs)(n.p,{children:["All logs are printed using the ",(0,s.jsx)(n.code,{children:"console.log"})," function."]}),"\n",(0,s.jsx)(n.p,{children:"If you also wish to consume the logs in another way (for example, to send them to a third-party error-tracking or logging tool), you can add one or more transports to the logger:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"logger.addTransport((level: 'debug'|'warn'|'info'|'error', log: string) => {\n // Do something\n})\n"})}),"\n",(0,s.jsx)(n.h2,{id:"logging-hook-deprecated",children:"Logging Hook (deprecated)"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["This hook is deprecated and will be removed in a next release. Use the ",(0,s.jsx)(n.code,{children:"Logger"})," service in a custom hook instead."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["FoalTS provides a convenient hook for logging debug messages: ",(0,s.jsx)(n.code,{children:"Log(message: string, options: LogOptions = {})"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"interface LogOptions {\n body?: boolean;\n params?: boolean;\n headers?: string[]|boolean;\n query?: boolean;\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, Log } from '@foal/core';\n\n@Log('AppController', {\n body: true,\n headers: [ 'X-CSRF-Token' ],\n params: true,\n query: true\n})\nexport class AppController {\n @Get()\n index() {\n return new HttpResponseOK();\n }\n}\n"})})]})}function g(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},64700:(e,n,o)=>{o.d(n,{A:()=>s});const s=o.p+"assets/images/dev-format-38d7e2e0c32975ec1126097a40e983df.png"},45349:(e,n,o)=>{o.d(n,{A:()=>s});const s=o.p+"assets/images/json-format-3fd891344d40c3b23aeb178c3eb94b6e.png"},72345:(e,n,o)=>{o.d(n,{A:()=>s});const s=o.p+"assets/images/raw-format-2dccbd1406071bb3c6b3758c4f6055fb.png"},28453:(e,n,o)=>{o.d(n,{R:()=>i,x:()=>l});var s=o(96540);const t={},r=s.createContext(t);function i(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/52f6d0d7.610113e0.js b/assets/js/52f6d0d7.610113e0.js new file mode 100644 index 0000000000..a79477b33c --- /dev/null +++ b/assets/js/52f6d0d7.610113e0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1177],{94521:(n,e,t)=>{t.r(e),t.d(e,{assets:()=>c,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>d,toc:()=>l});var r=t(74848),s=t(28453);const o={title:"Nuxt"},i=void 0,d={id:"frontend/nuxt.js",title:"Nuxt",description:"Nuxt is a frontend framework based on Vue.JS.",source:"@site/docs/frontend/nuxt.js.md",sourceDirName:"frontend",slug:"/frontend/nuxt.js",permalink:"/docs/frontend/nuxt.js",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/frontend/nuxt.js.md",tags:[],version:"current",frontMatter:{title:"Nuxt"},sidebar:"someSidebar",previous:{title:"Server-Side Rendering",permalink:"/docs/frontend/server-side-rendering"},next:{title:"404 Page",permalink:"/docs/frontend/not-found-page"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"Set Up",id:"set-up",level:2}];function a(n){const e={a:"a",code:"code",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",...(0,s.R)(),...n.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.a,{href:"https://nuxtjs.org/",children:"Nuxt"})," is a frontend framework based on ",(0,r.jsx)(e.a,{href:"http://vuejs.org",children:"Vue.JS"}),"."]}),"\n",(0,r.jsx)(e.p,{children:"This document explains how to use it in conjunction with FoalTS."}),"\n",(0,r.jsx)(e.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(e.p,{children:"Create your frontend and backend projects in two different folders."}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{children:"npx @foal/cli createapp backend\nnpx create-nuxt-app frontend\n"})}),"\n",(0,r.jsx)(e.h2,{id:"set-up",children:"Set Up"}),"\n",(0,r.jsxs)(e.ol,{children:["\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Open the file ",(0,r.jsx)(e.code,{children:"nuxt.config.js"})," in the ",(0,r.jsx)(e.code,{children:"frontend/"})," directory, move it to your ",(0,r.jsx)(e.code,{children:"backend/"})," directory and update its first lines as follows:"]}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-typescript",children:"module.exports = {\n srcDir: '../frontend',\n // ...\n}\n"})}),"\n"]}),"\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Go to your server directory and install ",(0,r.jsx)(e.code,{children:"nuxt"}),"."]}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{children:"npm install nuxt\n"})}),"\n"]}),"\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Then update your ",(0,r.jsx)(e.code,{children:"src/index.ts"})," file as follows:"]}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-typescript",children:"import { loadNuxt, build } from 'nuxt';\n// ...\n\nasync function main() {\n const isDev = process.env.NODE_ENV !== 'production';\n // We get Nuxt instance\n const nuxt = await loadNuxt(isDev ? 'dev' : 'start');\n\n if (isDev) {\n build(nuxt)\n }\n\n // ...\n\n const app = await createApp(AppController, {\n postMiddlewares: [\n nuxt.render\n ]\n });\n\n // ...\n}\n\nmain();\n\n"})}),"\n"]}),"\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Finally, delete the file ",(0,r.jsx)(e.code,{children:"index.html"})," in ",(0,r.jsx)(e.code,{children:"backend/public"}),"."]}),"\n"]}),"\n"]})]})}function u(n={}){const{wrapper:e}={...(0,s.R)(),...n.components};return e?(0,r.jsx)(e,{...n,children:(0,r.jsx)(a,{...n})}):a(n)}},28453:(n,e,t)=>{t.d(e,{R:()=>i,x:()=>d});var r=t(96540);const s={},o=r.createContext(s);function i(n){const e=r.useContext(o);return r.useMemo((function(){return"function"==typeof n?n(e):{...e,...n}}),[e,n])}function d(n){let e;return e=n.disableParentContext?"function"==typeof n.components?n.components(s):n.components||s:i(n.components),r.createElement(o.Provider,{value:e},n.children)}}}]); \ No newline at end of file diff --git a/assets/js/52f6d0d7.d69465c3.js b/assets/js/52f6d0d7.d69465c3.js deleted file mode 100644 index a91968e049..0000000000 --- a/assets/js/52f6d0d7.d69465c3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1177],{94521:(n,e,t)=>{t.r(e),t.d(e,{assets:()=>c,contentTitle:()=>i,default:()=>u,frontMatter:()=>o,metadata:()=>d,toc:()=>l});var r=t(74848),s=t(28453);const o={title:"Nuxt"},i=void 0,d={id:"frontend/nuxt.js",title:"Nuxt",description:"Nuxt is a frontend framework based on Vue.JS.",source:"@site/docs/frontend/nuxt.js.md",sourceDirName:"frontend",slug:"/frontend/nuxt.js",permalink:"/docs/frontend/nuxt.js",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/frontend/nuxt.js.md",tags:[],version:"current",frontMatter:{title:"Nuxt"},sidebar:"someSidebar",previous:{title:"Server-Side Rendering",permalink:"/docs/frontend/server-side-rendering"},next:{title:"404 Page",permalink:"/docs/frontend/not-found-page"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"Set Up",id:"set-up",level:2}];function a(n){const e={a:"a",code:"code",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",...(0,s.R)(),...n.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(e.p,{children:[(0,r.jsx)(e.a,{href:"https://nuxtjs.org/",children:"Nuxt"})," is a frontend framework based on ",(0,r.jsx)(e.a,{href:"http://vuejs.org",children:"Vue.JS"}),"."]}),"\n",(0,r.jsx)(e.p,{children:"This document explains how to use it in conjunction with FoalTS."}),"\n",(0,r.jsx)(e.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(e.p,{children:"Create your frontend and backend projects in two different folders."}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{children:"foal createapp backend\nnpx create-nuxt-app frontend\n"})}),"\n",(0,r.jsx)(e.h2,{id:"set-up",children:"Set Up"}),"\n",(0,r.jsxs)(e.ol,{children:["\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Open the file ",(0,r.jsx)(e.code,{children:"nuxt.config.js"})," in the ",(0,r.jsx)(e.code,{children:"frontend/"})," directory, move it to your ",(0,r.jsx)(e.code,{children:"backend/"})," directory and update its first lines as follows:"]}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-typescript",children:"module.exports = {\n srcDir: '../frontend',\n // ...\n}\n"})}),"\n"]}),"\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Go to your server directory and install ",(0,r.jsx)(e.code,{children:"nuxt"}),"."]}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{children:"npm install nuxt\n"})}),"\n"]}),"\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Then update your ",(0,r.jsx)(e.code,{children:"src/index.ts"})," file as follows:"]}),"\n",(0,r.jsx)(e.pre,{children:(0,r.jsx)(e.code,{className:"language-typescript",children:"import { loadNuxt, build } from 'nuxt';\n// ...\n\nasync function main() {\n const isDev = process.env.NODE_ENV !== 'production';\n // We get Nuxt instance\n const nuxt = await loadNuxt(isDev ? 'dev' : 'start');\n\n if (isDev) {\n build(nuxt)\n }\n\n // ...\n\n const app = await createApp(AppController, {\n postMiddlewares: [\n nuxt.render\n ]\n });\n\n // ...\n}\n\nmain();\n\n"})}),"\n"]}),"\n",(0,r.jsxs)(e.li,{children:["\n",(0,r.jsxs)(e.p,{children:["Finally, delete the file ",(0,r.jsx)(e.code,{children:"index.html"})," in ",(0,r.jsx)(e.code,{children:"backend/public"}),"."]}),"\n"]}),"\n"]})]})}function u(n={}){const{wrapper:e}={...(0,s.R)(),...n.components};return e?(0,r.jsx)(e,{...n,children:(0,r.jsx)(a,{...n})}):a(n)}},28453:(n,e,t)=>{t.d(e,{R:()=>i,x:()=>d});var r=t(96540);const s={},o=r.createContext(s);function i(n){const e=r.useContext(o);return r.useMemo((function(){return"function"==typeof n?n(e):{...e,...n}}),[e,n])}function d(n){let e;return e=n.disableParentContext?"function"==typeof n.components?n.components(s):n.components||s:i(n.components),r.createElement(o.Provider,{value:e},n.children)}}}]); \ No newline at end of file diff --git a/assets/js/5445446f.d7a0f048.js b/assets/js/5445446f.6ee5d89b.js similarity index 50% rename from assets/js/5445446f.d7a0f048.js rename to assets/js/5445446f.6ee5d89b.js index 657cedd46d..7bd0846a7b 100644 --- a/assets/js/5445446f.d7a0f048.js +++ b/assets/js/5445446f.6ee5d89b.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[6381],{69941:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>l});var t=n(74848),r=n(28453);const a={title:"Users"},i=void 0,o={id:"authentication/user-class",title:"Users",description:"The User Entity",source:"@site/docs/authentication/user-class.md",sourceDirName:"authentication",slug:"/authentication/user-class",permalink:"/docs/authentication/user-class",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/user-class.md",tags:[],version:"current",frontMatter:{title:"Users"},sidebar:"someSidebar",previous:{title:"Quick Start",permalink:"/docs/authentication/quick-start"},next:{title:"Passwords",permalink:"/docs/authentication/password-management"}},c={},l=[{value:"The User Entity",id:"the-user-entity",level:2},{value:"Creating Users ...",id:"creating-users-",level:2},{value:"... Programmatically",id:"-programmatically",level:3},{value:"... with a Shell Script (CLI)",id:"-with-a-shell-script-cli",level:3},{value:"Example (email and password)",id:"example-email-and-password",level:2},{value:"The User Entity",id:"the-user-entity-1",level:3},{value:"The create-user Shell Script",id:"the-create-user-shell-script",level:3},{value:"Using another ORM/ODM",id:"using-another-ormodm",level:2}];function d(e){const s={a:"a",blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.h2,{id:"the-user-entity",children:"The User Entity"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"import { BaseEntity, Entity, PrimaryGenerateColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number\n\n}\n"})}),"\n",(0,t.jsxs)(s.p,{children:["The ",(0,t.jsx)(s.code,{children:"User"})," entity is the core of the authentication and authorization system. It is a class that represents the ",(0,t.jsx)(s.code,{children:"user"})," table in the database and each of its instances represents a row in this table."]}),"\n",(0,t.jsxs)(s.p,{children:["The class definition is usually located in the file ",(0,t.jsx)(s.code,{children:"src/app/entities/user.entity.ts"}),". Its attributes represent the columns of the table."]}),"\n",(0,t.jsxs)(s.p,{children:["In FoalTS you can customize the ",(0,t.jsx)(s.code,{children:"User"})," class to suit your needs. The framework makes no assumptions about the attributes required by the user objects. Maybe you'll need a ",(0,t.jsx)(s.code,{children:"firstName"})," column, maybe not. Maybe the authentication will be processed with an email and a password or maybe you will use an authentication token. The choice is yours!"]}),"\n",(0,t.jsxs)(s.p,{children:["However, FoalTS provides abstract classes from which you can extend the ",(0,t.jsx)(s.code,{children:"User"})," entity. Such classes, such as ",(0,t.jsx)(s.code,{children:"UserWithPermissions"}),", have useful utilities for handling authentication and authorization, so that you do not have to reinvent the wheel."]}),"\n",(0,t.jsx)(s.h2,{id:"creating-users-",children:"Creating Users ..."}),"\n",(0,t.jsx)(s.p,{children:"There are several ways to create users."}),"\n",(0,t.jsx)(s.h3,{id:"-programmatically",children:"... Programmatically"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"import { User } from './src/app/entities';\n\nasync function main() {\n const user = new User();\n user.foo = 1;\n await user.save(); 1\n });\n}\n"})}),"\n",(0,t.jsx)(s.h3,{id:"-with-a-shell-script-cli",children:"... with a Shell Script (CLI)"}),"\n",(0,t.jsxs)(s.p,{children:["You can use the ",(0,t.jsx)(s.code,{children:"create-user"})," shell script (located in ",(0,t.jsx)(s.code,{children:"src/scripts"}),") to create a new user through the command line."]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-sh",children:"npm run build\nfoal run create-user\n"})}),"\n",(0,t.jsx)(s.h2,{id:"example-email-and-password",children:"Example (email and password)"}),"\n",(0,t.jsx)(s.p,{children:"This section describes how to create users with an email and a password."}),"\n",(0,t.jsx)(s.h3,{id:"the-user-entity-1",children:"The User Entity"}),"\n",(0,t.jsxs)(s.p,{children:["Go to ",(0,t.jsx)(s.code,{children:"src/app/entities/user.entity.ts"})," and add two new columns: an email and a password."]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"import { hashPassword } from '@foal/core';\nimport { Column, Entity, PrimaryGeneratedColumn, BeforeInsert, BeforeUpdate } from 'typeorm';\n\n@Entity()\nexport class User {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n\n @BeforeInsert()\n @BeforeUpdate()\n async hashPassword() {\n // Hash the password before storing it in the database\n this.password = await hashPassword(this.password);\n }\n\n}\n\n"})}),"\n",(0,t.jsxs)(s.blockquote,{children:["\n",(0,t.jsxs)(s.p,{children:["Note: The ",(0,t.jsx)(s.code,{children:"BeforeInsert"})," and ",(0,t.jsx)(s.code,{children:"BeforeUpdate"})," are TypeORM decorators for Entity Listeners that run before the entity is saved in the db. In this example they take care of hashing the password. More info about ",(0,t.jsx)(s.code,{children:"Entity Listeners"})," in the ",(0,t.jsx)(s.a,{href:"https://typeorm.io/#/listeners-and-subscribers",children:"TypeORM docs"})]}),"\n"]}),"\n",(0,t.jsx)(s.h3,{id:"the-create-user-shell-script",children:"The create-user Shell Script"}),"\n",(0,t.jsxs)(s.p,{children:["Running the ",(0,t.jsx)(s.code,{children:"create-user"})," script will result in an error since we do not provide an email and a password as arguments."]}),"\n",(0,t.jsxs)(s.p,{children:["Go to ",(0,t.jsx)(s.code,{children:"src/scripts/create-user.ts"})," and replace its content with the following lines:"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { hashPassword } from '@foal/core';\n\n// App\nimport { User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' },\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport async function main(args) {\n await dataSource.initialize();\n\n try {\n const user = new User();\n user.email = args.email;\n user.password = await hashPassword(args.password);\n\n console.log(await user.save());\n } catch (error: any) {\n console.error(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,t.jsx)(s.p,{children:"You can now create a new user with these commands:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-sh",children:"npm run build\nfoal run create-user email=mary@foalts.org password=mary_password\n"})}),"\n",(0,t.jsx)(s.h2,{id:"using-another-ormodm",children:"Using another ORM/ODM"}),"\n",(0,t.jsxs)(s.p,{children:["In this document, we used TypeORM to define the ",(0,t.jsx)(s.code,{children:"User"})," class and the ",(0,t.jsx)(s.code,{children:"create-user"})," shell script. However, you can still use another ORM/ODM if you want to."]})]})}function h(e={}){const{wrapper:s}={...(0,r.R)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},28453:(e,s,n)=>{n.d(s,{R:()=>i,x:()=>o});var t=n(96540);const r={},a=t.createContext(r);function i(e){const s=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function o(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),t.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[6381],{69941:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>l});var t=n(74848),r=n(28453);const a={title:"Users"},i=void 0,o={id:"authentication/user-class",title:"Users",description:"The User Entity",source:"@site/docs/authentication/user-class.md",sourceDirName:"authentication",slug:"/authentication/user-class",permalink:"/docs/authentication/user-class",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/user-class.md",tags:[],version:"current",frontMatter:{title:"Users"},sidebar:"someSidebar",previous:{title:"Quick Start",permalink:"/docs/authentication/quick-start"},next:{title:"Passwords",permalink:"/docs/authentication/password-management"}},c={},l=[{value:"The User Entity",id:"the-user-entity",level:2},{value:"Creating Users ...",id:"creating-users-",level:2},{value:"... Programmatically",id:"-programmatically",level:3},{value:"... with a Shell Script (CLI)",id:"-with-a-shell-script-cli",level:3},{value:"Example (email and password)",id:"example-email-and-password",level:2},{value:"The User Entity",id:"the-user-entity-1",level:3},{value:"The create-user Shell Script",id:"the-create-user-shell-script",level:3},{value:"Using another ORM/ODM",id:"using-another-ormodm",level:2}];function d(e){const s={a:"a",blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.h2,{id:"the-user-entity",children:"The User Entity"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"import { BaseEntity, Entity, PrimaryGenerateColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number\n\n}\n"})}),"\n",(0,t.jsxs)(s.p,{children:["The ",(0,t.jsx)(s.code,{children:"User"})," entity is the core of the authentication and authorization system. It is a class that represents the ",(0,t.jsx)(s.code,{children:"user"})," table in the database and each of its instances represents a row in this table."]}),"\n",(0,t.jsxs)(s.p,{children:["The class definition is usually located in the file ",(0,t.jsx)(s.code,{children:"src/app/entities/user.entity.ts"}),". Its attributes represent the columns of the table."]}),"\n",(0,t.jsxs)(s.p,{children:["In FoalTS you can customize the ",(0,t.jsx)(s.code,{children:"User"})," class to suit your needs. The framework makes no assumptions about the attributes required by the user objects. Maybe you'll need a ",(0,t.jsx)(s.code,{children:"firstName"})," column, maybe not. Maybe the authentication will be processed with an email and a password or maybe you will use an authentication token. The choice is yours!"]}),"\n",(0,t.jsxs)(s.p,{children:["However, FoalTS provides abstract classes from which you can extend the ",(0,t.jsx)(s.code,{children:"User"})," entity. Such classes, such as ",(0,t.jsx)(s.code,{children:"UserWithPermissions"}),", have useful utilities for handling authentication and authorization, so that you do not have to reinvent the wheel."]}),"\n",(0,t.jsx)(s.h2,{id:"creating-users-",children:"Creating Users ..."}),"\n",(0,t.jsx)(s.p,{children:"There are several ways to create users."}),"\n",(0,t.jsx)(s.h3,{id:"-programmatically",children:"... Programmatically"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"import { User } from './src/app/entities';\n\nasync function main() {\n const user = new User();\n user.foo = 1;\n await user.save(); 1\n });\n}\n"})}),"\n",(0,t.jsx)(s.h3,{id:"-with-a-shell-script-cli",children:"... with a Shell Script (CLI)"}),"\n",(0,t.jsxs)(s.p,{children:["You can use the ",(0,t.jsx)(s.code,{children:"create-user"})," shell script (located in ",(0,t.jsx)(s.code,{children:"src/scripts"}),") to create a new user through the command line."]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-sh",children:"npm run build\nnpx foal run create-user\n"})}),"\n",(0,t.jsx)(s.h2,{id:"example-email-and-password",children:"Example (email and password)"}),"\n",(0,t.jsx)(s.p,{children:"This section describes how to create users with an email and a password."}),"\n",(0,t.jsx)(s.h3,{id:"the-user-entity-1",children:"The User Entity"}),"\n",(0,t.jsxs)(s.p,{children:["Go to ",(0,t.jsx)(s.code,{children:"src/app/entities/user.entity.ts"})," and add two new columns: an email and a password."]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"import { hashPassword } from '@foal/core';\nimport { Column, Entity, PrimaryGeneratedColumn, BeforeInsert, BeforeUpdate } from 'typeorm';\n\n@Entity()\nexport class User {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n\n @BeforeInsert()\n @BeforeUpdate()\n async hashPassword() {\n // Hash the password before storing it in the database\n this.password = await hashPassword(this.password);\n }\n\n}\n\n"})}),"\n",(0,t.jsxs)(s.blockquote,{children:["\n",(0,t.jsxs)(s.p,{children:["Note: The ",(0,t.jsx)(s.code,{children:"BeforeInsert"})," and ",(0,t.jsx)(s.code,{children:"BeforeUpdate"})," are TypeORM decorators for Entity Listeners that run before the entity is saved in the db. In this example they take care of hashing the password. More info about ",(0,t.jsx)(s.code,{children:"Entity Listeners"})," in the ",(0,t.jsx)(s.a,{href:"https://typeorm.io/#/listeners-and-subscribers",children:"TypeORM docs"})]}),"\n"]}),"\n",(0,t.jsx)(s.h3,{id:"the-create-user-shell-script",children:"The create-user Shell Script"}),"\n",(0,t.jsxs)(s.p,{children:["Running the ",(0,t.jsx)(s.code,{children:"create-user"})," script will result in an error since we do not provide an email and a password as arguments."]}),"\n",(0,t.jsxs)(s.p,{children:["Go to ",(0,t.jsx)(s.code,{children:"src/scripts/create-user.ts"})," and replace its content with the following lines:"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { hashPassword } from '@foal/core';\n\n// App\nimport { User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' },\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport async function main(args) {\n await dataSource.initialize();\n\n try {\n const user = new User();\n user.email = args.email;\n user.password = await hashPassword(args.password);\n\n console.log(await user.save());\n } catch (error: any) {\n console.error(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,t.jsx)(s.p,{children:"You can now create a new user with these commands:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-sh",children:"npm run build\nnpx foal run create-user email=mary@foalts.org password=mary_password\n"})}),"\n",(0,t.jsx)(s.h2,{id:"using-another-ormodm",children:"Using another ORM/ODM"}),"\n",(0,t.jsxs)(s.p,{children:["In this document, we used TypeORM to define the ",(0,t.jsx)(s.code,{children:"User"})," class and the ",(0,t.jsx)(s.code,{children:"create-user"})," shell script. However, you can still use another ORM/ODM if you want to."]})]})}function h(e={}){const{wrapper:s}={...(0,r.R)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},28453:(e,s,n)=>{n.d(s,{R:()=>i,x:()=>o});var t=n(96540);const r={},a=t.createContext(r);function i(e){const s=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function o(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),t.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/638a37f1.7e348230.js b/assets/js/638a37f1.7e348230.js new file mode 100644 index 0000000000..50dedbecbe --- /dev/null +++ b/assets/js/638a37f1.7e348230.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3830],{86037:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>t,default:()=>h,frontMatter:()=>c,metadata:()=>i,toc:()=>a});var l=r(74848),s=r(28453);const c={title:"Code Generation"},t=void 0,i={id:"cli/code-generation",title:"Code Generation",description:"Create a project",source:"@site/docs/cli/code-generation.md",sourceDirName:"cli",slug:"/cli/code-generation",permalink:"/docs/cli/code-generation",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/cli/code-generation.md",tags:[],version:"current",frontMatter:{title:"Code Generation"},sidebar:"someSidebar",previous:{title:"Shell Scripts",permalink:"/docs/cli/shell-scripts"},next:{title:"Linting",permalink:"/docs/cli/linting-and-code-style"}},o={},a=[{value:"Create a project",id:"create-a-project",level:2},{value:"Create a controller",id:"create-a-controller",level:2},{value:"The --register flag",id:"the---register-flag",level:3},{value:"Create an entity",id:"create-an-entity",level:2},{value:"Create REST API",id:"create-rest-api",level:2},{value:"The --register flag",id:"the---register-flag-1",level:3},{value:"Create a hook",id:"create-a-hook",level:2},{value:"Create a script",id:"create-a-script",level:2},{value:"Create a service",id:"create-a-service",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(n.h2,{id:"create-a-project",children:"Create a project"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx @foal/cli createapp my-app\n"})}),"\n",(0,l.jsx)(n.p,{children:"Create a new directory with all the required files to get started."}),"\n",(0,l.jsxs)(n.p,{children:["If you specify the flag ",(0,l.jsx)(n.code,{children:"--mongodb"}),", the CLI will generate a new project using MongoDB instead of SQLite."]}),"\n",(0,l.jsxs)(n.p,{children:["If you specify the flag ",(0,l.jsx)(n.code,{children:"--yaml"}),", the new project will use YAML format for its configuration files. You can find more information ",(0,l.jsx)(n.a,{href:"/docs/architecture/configuration",children:"here"}),"."]}),"\n",(0,l.jsx)(n.h2,{id:"create-a-controller",children:"Create a controller"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g controller \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new controller in ",(0,l.jsx)(n.code,{children:"./src/app/controllers"}),", in ",(0,l.jsx)(n.code,{children:"./controllers"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g controller auth\nnpx foal g controller api/product\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- controllers/\n |- api/\n | |- product.controller.ts\n | '- index.ts\n |- auth.controller.ts\n '- index.ts\n"})}),"\n",(0,l.jsxs)(n.h3,{id:"the---register-flag",children:["The ",(0,l.jsx)(n.code,{children:"--register"})," flag"]}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g controller --register\n"})}),"\n",(0,l.jsxs)(n.p,{children:["If you wish to automatically create a new route attached to this controller, you can use the ",(0,l.jsx)(n.code,{children:"--register"})," flag to do so."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g controller api --register\nnpx foal g controller api/product --register\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n |- controllers/\n | |- api/\n | | |- product.controller.ts\n | | '- index.ts\n | |- api.controller.ts\n | '- index.ts\n '- app.controller.ts\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-typescript",children:"export class AppController {\n subControllers = [\n controller('/api', ApiController)\n ]\n}\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"api.controller.ts"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-typescript",children:"export class ApiController {\n subControllers = [\n controller('/product', ProductController)\n ]\n}\n"})}),"\n",(0,l.jsx)(n.h2,{id:"create-an-entity",children:"Create an entity"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g entity \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new entity in ",(0,l.jsx)(n.code,{children:"./src/app/entities"}),", in ",(0,l.jsx)(n.code,{children:"./entities"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g entity user\nnpx foal g entity business/product\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- entities/\n |- business/\n | |- product.entity.ts\n | '- index.ts\n |- user.entity.ts\n '- index.ts\n"})}),"\n",(0,l.jsx)(n.h2,{id:"create-rest-api",children:"Create REST API"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g rest-api \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new controller and a new entity to build a basic REST API. Depending on which directories are found, they will be generated in ",(0,l.jsx)(n.code,{children:"src/app/{entities}|{controllers}/"})," or in ",(0,l.jsx)(n.code,{children:"{entities}|{controllers}/"}),"."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g rest-api order\nnpx foal g rest-api api/product\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n |- controllers/\n | |- api/\n | | |- product.controller.ts\n | | '- index.ts\n | |- order.controller.ts\n | '- index.ts\n '- entities/\n |- index.entity.ts\n |- order.entity.ts\n '- index.ts\n"})}),"\n",(0,l.jsxs)(n.h3,{id:"the---register-flag-1",children:["The ",(0,l.jsx)(n.code,{children:"--register"})," flag"]}),"\n",(0,l.jsxs)(n.p,{children:["If you wish to automatically create a new route attached to this controller, you can use the ",(0,l.jsx)(n.code,{children:"--register"})," flag to do so (see ",(0,l.jsx)(n.a,{href:"#create-a-controller",children:"create-a-controller"}),")."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g controller api --register\nnpx foal g controller api/product --register\n"})}),"\n",(0,l.jsxs)(n.p,{children:["See the page ",(0,l.jsx)(n.a,{href:"/docs/common/rest-blueprints",children:"REST Blueprints"})," for more details."]}),"\n",(0,l.jsx)(n.h2,{id:"create-a-hook",children:"Create a hook"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g hook \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new hook in ",(0,l.jsx)(n.code,{children:"./src/app/hooks"}),", in ",(0,l.jsx)(n.code,{children:"./hooks"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g hook log\nnpx foal g hook auth/admin-required\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- hooks/\n |- auth/\n | |- admin-required.hook.ts\n | '- index.ts\n |- log.hook.ts\n '- index.ts\n"})}),"\n",(0,l.jsx)(n.h2,{id:"create-a-script",children:"Create a script"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g script \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new shell script in ",(0,l.jsx)(n.code,{children:"src/scripts"})," regardless of where you run the command."]}),"\n",(0,l.jsx)(n.h2,{id:"create-a-service",children:"Create a service"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g service \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new service in ",(0,l.jsx)(n.code,{children:"./src/app/services"}),", in ",(0,l.jsx)(n.code,{children:"./services"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"npx foal g service auth\nnpx foal g service apis/github\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- services/\n |- apis/\n | '- github.service.ts\n | '- index.ts\n |- auth.service.ts\n '- index.ts\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,l.jsx)(n,{...e,children:(0,l.jsx)(d,{...e})}):d(e)}},28453:(e,n,r)=>{r.d(n,{R:()=>t,x:()=>i});var l=r(96540);const s={},c=l.createContext(s);function t(e){const n=l.useContext(c);return l.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),l.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/638a37f1.a563e66b.js b/assets/js/638a37f1.a563e66b.js deleted file mode 100644 index c386966fe7..0000000000 --- a/assets/js/638a37f1.a563e66b.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3830],{86037:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>t,default:()=>h,frontMatter:()=>c,metadata:()=>i,toc:()=>a});var l=r(74848),s=r(28453);const c={title:"Code Generation"},t=void 0,i={id:"cli/code-generation",title:"Code Generation",description:"Create a project",source:"@site/docs/cli/code-generation.md",sourceDirName:"cli",slug:"/cli/code-generation",permalink:"/docs/cli/code-generation",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/cli/code-generation.md",tags:[],version:"current",frontMatter:{title:"Code Generation"},sidebar:"someSidebar",previous:{title:"Shell Scripts",permalink:"/docs/cli/shell-scripts"},next:{title:"Linting",permalink:"/docs/cli/linting-and-code-style"}},o={},a=[{value:"Create a project",id:"create-a-project",level:2},{value:"Create a controller",id:"create-a-controller",level:2},{value:"The --register flag",id:"the---register-flag",level:3},{value:"Create an entity",id:"create-an-entity",level:2},{value:"Create REST API",id:"create-rest-api",level:2},{value:"The --register flag",id:"the---register-flag-1",level:3},{value:"Create a hook",id:"create-a-hook",level:2},{value:"Create a script",id:"create-a-script",level:2},{value:"Create a service",id:"create-a-service",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(n.h2,{id:"create-a-project",children:"Create a project"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal createapp my-app\n"})}),"\n",(0,l.jsx)(n.p,{children:"Create a new directory with all the required files to get started."}),"\n",(0,l.jsxs)(n.p,{children:["If you specify the flag ",(0,l.jsx)(n.code,{children:"--mongodb"}),", the CLI will generate a new project using MongoDB instead of SQLite."]}),"\n",(0,l.jsxs)(n.p,{children:["If you specify the flag ",(0,l.jsx)(n.code,{children:"--yaml"}),", the new project will use YAML format for its configuration files. You can find more information ",(0,l.jsx)(n.a,{href:"/docs/architecture/configuration",children:"here"}),"."]}),"\n",(0,l.jsx)(n.h2,{id:"create-a-controller",children:"Create a controller"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g controller \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new controller in ",(0,l.jsx)(n.code,{children:"./src/app/controllers"}),", in ",(0,l.jsx)(n.code,{children:"./controllers"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g controller auth\nfoal g controller api/product\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- controllers/\n |- api/\n | |- product.controller.ts\n | '- index.ts\n |- auth.controller.ts\n '- index.ts\n"})}),"\n",(0,l.jsxs)(n.h3,{id:"the---register-flag",children:["The ",(0,l.jsx)(n.code,{children:"--register"})," flag"]}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g controller --register\n"})}),"\n",(0,l.jsxs)(n.p,{children:["If you wish to automatically create a new route attached to this controller, you can use the ",(0,l.jsx)(n.code,{children:"--register"})," flag to do so."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g controller api --register\nfoal g controller api/product --register\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n |- controllers/\n | |- api/\n | | |- product.controller.ts\n | | '- index.ts\n | |- api.controller.ts\n | '- index.ts\n '- app.controller.ts\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"app.controller.ts"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-typescript",children:"export class AppController {\n subControllers = [\n controller('/api', ApiController)\n ]\n}\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"api.controller.ts"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-typescript",children:"export class ApiController {\n subControllers = [\n controller('/product', ProductController)\n ]\n}\n"})}),"\n",(0,l.jsx)(n.h2,{id:"create-an-entity",children:"Create an entity"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g entity \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new entity in ",(0,l.jsx)(n.code,{children:"./src/app/entities"}),", in ",(0,l.jsx)(n.code,{children:"./entities"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g entity user\nfoal g entity business/product\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- entities/\n |- business/\n | |- product.entity.ts\n | '- index.ts\n |- user.entity.ts\n '- index.ts\n"})}),"\n",(0,l.jsx)(n.h2,{id:"create-rest-api",children:"Create REST API"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g rest-api \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new controller and a new entity to build a basic REST API. Depending on which directories are found, they will be generated in ",(0,l.jsx)(n.code,{children:"src/app/{entities}|{controllers}/"})," or in ",(0,l.jsx)(n.code,{children:"{entities}|{controllers}/"}),"."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g rest-api order\nfoal g rest-api api/product\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n |- controllers/\n | |- api/\n | | |- product.controller.ts\n | | '- index.ts\n | |- order.controller.ts\n | '- index.ts\n '- entities/\n |- index.entity.ts\n |- order.entity.ts\n '- index.ts\n"})}),"\n",(0,l.jsxs)(n.h3,{id:"the---register-flag-1",children:["The ",(0,l.jsx)(n.code,{children:"--register"})," flag"]}),"\n",(0,l.jsxs)(n.p,{children:["If you wish to automatically create a new route attached to this controller, you can use the ",(0,l.jsx)(n.code,{children:"--register"})," flag to do so (see ",(0,l.jsx)(n.a,{href:"#create-a-controller",children:"create-a-controller"}),")."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g controller api --register\nfoal g controller api/product --register\n"})}),"\n",(0,l.jsxs)(n.p,{children:["See the page ",(0,l.jsx)(n.a,{href:"/docs/common/rest-blueprints",children:"REST Blueprints"})," for more details."]}),"\n",(0,l.jsx)(n.h2,{id:"create-a-hook",children:"Create a hook"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g hook \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new hook in ",(0,l.jsx)(n.code,{children:"./src/app/hooks"}),", in ",(0,l.jsx)(n.code,{children:"./hooks"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g hook log\nfoal g hook auth/admin-required\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- hooks/\n |- auth/\n | |- admin-required.hook.ts\n | '- index.ts\n |- log.hook.ts\n '- index.ts\n"})}),"\n",(0,l.jsx)(n.h2,{id:"create-a-script",children:"Create a script"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g script \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new shell script in ",(0,l.jsx)(n.code,{children:"src/scripts"})," regardless of where you run the command."]}),"\n",(0,l.jsx)(n.h2,{id:"create-a-service",children:"Create a service"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g service \n"})}),"\n",(0,l.jsxs)(n.p,{children:["Create a new service in ",(0,l.jsx)(n.code,{children:"./src/app/services"}),", in ",(0,l.jsx)(n.code,{children:"./services"})," or in the current directory depending on which folders are found."]}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Example"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-shell",children:"foal g service auth\nfoal g service apis/github\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.em,{children:"Output"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{children:"src/\n '- app/\n '- services/\n |- apis/\n | '- github.service.ts\n | '- index.ts\n |- auth.service.ts\n '- index.ts\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,l.jsx)(n,{...e,children:(0,l.jsx)(d,{...e})}):d(e)}},28453:(e,n,r)=>{r.d(n,{R:()=>t,x:()=>i});var l=r(96540);const s={},c=l.createContext(s);function t(e){const n=l.useContext(c);return l.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),l.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/65b85fac.30341af2.js b/assets/js/65b85fac.30341af2.js new file mode 100644 index 0000000000..5f71081c66 --- /dev/null +++ b/assets/js/65b85fac.30341af2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5975],{28582:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var r=n(74848),a=n(28453);const i={title:"Image Upload and Download",id:"tuto-12-file-upload",slug:"12-file-upload"},s=void 0,o={id:"tutorials/real-world-example-with-react/tuto-12-file-upload",title:"Image Upload and Download",description:"The next step in this tutorial is to allow users to upload a profile picture. This image will be displayed on the homepage in front of each author's story.",source:"@site/docs/tutorials/real-world-example-with-react/12-file-upload.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/12-file-upload",permalink:"/docs/tutorials/real-world-example-with-react/12-file-upload",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/12-file-upload.md",tags:[],version:"current",sidebarPosition:12,frontMatter:{title:"Image Upload and Download",id:"tuto-12-file-upload",slug:"12-file-upload"},sidebar:"someSidebar",previous:{title:"Adding Sign Up",permalink:"/docs/tutorials/real-world-example-with-react/11-sign-up"},next:{title:"CSRF Protection",permalink:"/docs/tutorials/real-world-example-with-react/13-csrf"}},l={},d=[{value:"Server Side",id:"server-side",level:2},{value:"Client Side",id:"client-side",level:2}];function c(e){const t={a:"a",blockquote:"blockquote",code:"code",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.p,{children:"The next step in this tutorial is to allow users to upload a profile picture. This image will be displayed on the homepage in front of each author's story."}),"\n",(0,r.jsxs)(t.p,{children:["To do this, you will use Foal's storage system. It allows you to validate and save the files uploaded by the client. These files can be saved to your local drive or in the cloud using AWS S3. We won't use the cloud feature in this tutorial, but you can find out how to configure it ",(0,r.jsx)(t.a,{href:"/docs/common/file-storage/local-and-cloud-storage",children:"here"}),"."]}),"\n",(0,r.jsx)(t.h2,{id:"server-side",children:"Server Side"}),"\n",(0,r.jsx)(t.p,{children:"First, install the package."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm install @foal/storage\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Update the configuration in ",(0,r.jsx)(t.code,{children:"config/default.json"})," to specify the location of files that the disk manager can access."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-json",children:'{\n "port": "env(PORT)",\n "settings": {\n ...\n "disk": {\n "local": {\n "directory": "assets"\n }\n }\n },\n ...\n}\n'})}),"\n",(0,r.jsxs)(t.p,{children:["Then create the directory ",(0,r.jsx)(t.code,{children:"assets/images/profiles/uploaded"})," where the profile images will be uploaded. Download the default profile image ",(0,r.jsx)(t.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:n(97529).A+"",children:"here"})," and place it in the ",(0,r.jsx)(t.code,{children:"assets/images/profiles"})," folder with the name ",(0,r.jsx)(t.code,{children:"default.png"}),"."]}),"\n",(0,r.jsx)(t.p,{children:"You are ready to create the controller. Generate a new one."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npx foal generate controller api/profile --register\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Open the new file ",(0,r.jsx)(t.code,{children:"profile.controller.ts"})," and add two new routes."]}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"API endpoint"}),(0,r.jsx)(t.th,{children:"Method"}),(0,r.jsx)(t.th,{children:"Description"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"/api/profile/avatar"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"GET"})}),(0,r.jsxs)(t.td,{children:["Retrieves the user's profile image. If the optional query parameter ",(0,r.jsx)(t.code,{children:"userId"})," is provided, the server returns the avatar of that user. Otherwise, it returns the avatar of the current user. If no user is authenticated or has no profile picture, a default image is returned."]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"/api/profile"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"POST"})}),(0,r.jsxs)(t.td,{children:["Updates the user profile. A ",(0,r.jsx)(t.code,{children:"name"})," field and an optional ",(0,r.jsx)(t.code,{children:"avatar"})," file are expected."]})]})]})]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { Context, dependency, File, Get, HttpResponseNoContent, Post, UserRequired, ValidateQueryParam } from '@foal/core';\nimport { Disk, ParseAndValidateFiles } from '@foal/storage';\nimport { User } from '../../entities';\n\nexport class ProfileController {\n @dependency\n disk: Disk;\n\n @Get('/avatar')\n @ValidateQueryParam('userId', { type: 'number' }, { required: false })\n async readProfileImage(ctx: Context) {\n let user = ctx.user;\n\n const userId: number|undefined = ctx.request.query.userId;\n if (userId !== undefined) {\n user = await User.findOneBy({ id: userId })\n }\n\n if (!user || !user.avatar) {\n return this.disk.createHttpResponse('images/profiles/default.png');\n }\n\n return this.disk.createHttpResponse(user.avatar);\n }\n\n @Post()\n @UserRequired()\n @ParseAndValidateFiles(\n {\n avatar: { required: false, saveTo: 'images/profiles/uploaded' }\n },\n {\n type: 'object',\n properties: {\n name: { type: 'string', maxLength: 255 }\n },\n required: ['name']\n }\n )\n async updateProfileImage(ctx: Context) {\n ctx.user.name = ctx.request.body.name;\n\n // Warning: File must be imported from `@foal/core`.\n const file: File|undefined = ctx.files.get('avatar')[0];\n if (file) {\n if (ctx.user.avatar) {\n await this.disk.delete(ctx.user.avatar);\n }\n ctx.user.avatar = file.path;\n }\n\n await ctx.user.save();\n\n return new HttpResponseNoContent();\n }\n\n}\n\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Go to ",(0,r.jsx)(t.a,{href:"http://localhost:3001/swagger",children:"http://localhost:3001/swagger"})," and try to upload a profile picture. You must be logged in first."]}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsxs)(t.p,{children:["You may have noticed the ",(0,r.jsx)(t.code,{children:"@dependency"})," decorator for setting the ",(0,r.jsx)(t.code,{children:"disk: Disk"})," property. This mechanism is called dependency injection and is particularly useful in unit testing. You can read more about it ",(0,r.jsx)(t.a,{href:"/docs/architecture/architecture-overview",children:"here"})]}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"client-side",children:"Client Side"}),"\n",(0,r.jsxs)(t.p,{children:["On the client side, downloading the profile image is handled in the ",(0,r.jsx)(t.code,{children:"ProfileHeader.tsx"})," and ",(0,r.jsx)(t.code,{children:"requests/profile.ts"})," files."]}),"\n",(0,r.jsxs)(t.p,{children:["Open the latter and implement the ",(0,r.jsx)(t.code,{children:"updateProfile"})," function."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import axios from 'axios';\n\nexport async function updateProfile(username: string, avatar: File|null): Promise {\n const formData = new FormData();\n formData.set('name', username);\n if (avatar) {\n formData.set('avatar', avatar);\n }\n\n await axios.post('/api/profile', formData, {\n headers: {\n 'content-type': 'multipart/form-data'\n }\n });\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Now, if you go back to ",(0,r.jsx)(t.a,{href:"http://localhost:3000/profile",children:"http://localhost:3000/profile"}),", you should be able to upload your profile picture."]})]})}function h(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},97529:(e,t,n)=>{n.d(t,{A:()=>r});const r=n.p+"assets/files/default-9490f4915bf9aca8c77fd98e411f2e2c.png"},28453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>o});var r=n(96540);const a={},i=r.createContext(a);function s(e){const t=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/65b85fac.95392571.js b/assets/js/65b85fac.95392571.js deleted file mode 100644 index a8b4cbfc46..0000000000 --- a/assets/js/65b85fac.95392571.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5975],{28582:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var r=n(74848),a=n(28453);const i={title:"Image Upload and Download",id:"tuto-12-file-upload",slug:"12-file-upload"},s=void 0,o={id:"tutorials/real-world-example-with-react/tuto-12-file-upload",title:"Image Upload and Download",description:"The next step in this tutorial is to allow users to upload a profile picture. This image will be displayed on the homepage in front of each author's story.",source:"@site/docs/tutorials/real-world-example-with-react/12-file-upload.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/12-file-upload",permalink:"/docs/tutorials/real-world-example-with-react/12-file-upload",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/12-file-upload.md",tags:[],version:"current",sidebarPosition:12,frontMatter:{title:"Image Upload and Download",id:"tuto-12-file-upload",slug:"12-file-upload"},sidebar:"someSidebar",previous:{title:"Adding Sign Up",permalink:"/docs/tutorials/real-world-example-with-react/11-sign-up"},next:{title:"CSRF Protection",permalink:"/docs/tutorials/real-world-example-with-react/13-csrf"}},l={},d=[{value:"Server Side",id:"server-side",level:2},{value:"Client Side",id:"client-side",level:2}];function c(e){const t={a:"a",blockquote:"blockquote",code:"code",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.p,{children:"The next step in this tutorial is to allow users to upload a profile picture. This image will be displayed on the homepage in front of each author's story."}),"\n",(0,r.jsxs)(t.p,{children:["To do this, you will use Foal's storage system. It allows you to validate and save the files uploaded by the client. These files can be saved to your local drive or in the cloud using AWS S3. We won't use the cloud feature in this tutorial, but you can find out how to configure it ",(0,r.jsx)(t.a,{href:"/docs/common/file-storage/local-and-cloud-storage",children:"here"}),"."]}),"\n",(0,r.jsx)(t.h2,{id:"server-side",children:"Server Side"}),"\n",(0,r.jsx)(t.p,{children:"First, install the package."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm install @foal/storage\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Update the configuration in ",(0,r.jsx)(t.code,{children:"config/default.json"})," to specify the location of files that the disk manager can access."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-json",children:'{\n "port": "env(PORT)",\n "settings": {\n ...\n "disk": {\n "local": {\n "directory": "assets"\n }\n }\n },\n ...\n}\n'})}),"\n",(0,r.jsxs)(t.p,{children:["Then create the directory ",(0,r.jsx)(t.code,{children:"assets/images/profiles/uploaded"})," where the profile images will be uploaded. Download the default profile image ",(0,r.jsx)(t.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:n(97529).A+"",children:"here"})," and place it in the ",(0,r.jsx)(t.code,{children:"assets/images/profiles"})," folder with the name ",(0,r.jsx)(t.code,{children:"default.png"}),"."]}),"\n",(0,r.jsx)(t.p,{children:"You are ready to create the controller. Generate a new one."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"foal generate controller api/profile --register\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Open the new file ",(0,r.jsx)(t.code,{children:"profile.controller.ts"})," and add two new routes."]}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"API endpoint"}),(0,r.jsx)(t.th,{children:"Method"}),(0,r.jsx)(t.th,{children:"Description"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"/api/profile/avatar"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"GET"})}),(0,r.jsxs)(t.td,{children:["Retrieves the user's profile image. If the optional query parameter ",(0,r.jsx)(t.code,{children:"userId"})," is provided, the server returns the avatar of that user. Otherwise, it returns the avatar of the current user. If no user is authenticated or has no profile picture, a default image is returned."]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"/api/profile"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"POST"})}),(0,r.jsxs)(t.td,{children:["Updates the user profile. A ",(0,r.jsx)(t.code,{children:"name"})," field and an optional ",(0,r.jsx)(t.code,{children:"avatar"})," file are expected."]})]})]})]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { Context, dependency, File, Get, HttpResponseNoContent, Post, UserRequired, ValidateQueryParam } from '@foal/core';\nimport { Disk, ParseAndValidateFiles } from '@foal/storage';\nimport { User } from '../../entities';\n\nexport class ProfileController {\n @dependency\n disk: Disk;\n\n @Get('/avatar')\n @ValidateQueryParam('userId', { type: 'number' }, { required: false })\n async readProfileImage(ctx: Context) {\n let user = ctx.user;\n\n const userId: number|undefined = ctx.request.query.userId;\n if (userId !== undefined) {\n user = await User.findOneBy({ id: userId })\n }\n\n if (!user || !user.avatar) {\n return this.disk.createHttpResponse('images/profiles/default.png');\n }\n\n return this.disk.createHttpResponse(user.avatar);\n }\n\n @Post()\n @UserRequired()\n @ParseAndValidateFiles(\n {\n avatar: { required: false, saveTo: 'images/profiles/uploaded' }\n },\n {\n type: 'object',\n properties: {\n name: { type: 'string', maxLength: 255 }\n },\n required: ['name']\n }\n )\n async updateProfileImage(ctx: Context) {\n ctx.user.name = ctx.request.body.name;\n\n // Warning: File must be imported from `@foal/core`.\n const file: File|undefined = ctx.files.get('avatar')[0];\n if (file) {\n if (ctx.user.avatar) {\n await this.disk.delete(ctx.user.avatar);\n }\n ctx.user.avatar = file.path;\n }\n\n await ctx.user.save();\n\n return new HttpResponseNoContent();\n }\n\n}\n\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Go to ",(0,r.jsx)(t.a,{href:"http://localhost:3001/swagger",children:"http://localhost:3001/swagger"})," and try to upload a profile picture. You must be logged in first."]}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsxs)(t.p,{children:["You may have noticed the ",(0,r.jsx)(t.code,{children:"@dependency"})," decorator for setting the ",(0,r.jsx)(t.code,{children:"disk: Disk"})," property. This mechanism is called dependency injection and is particularly useful in unit testing. You can read more about it ",(0,r.jsx)(t.a,{href:"/docs/architecture/architecture-overview",children:"here"})]}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"client-side",children:"Client Side"}),"\n",(0,r.jsxs)(t.p,{children:["On the client side, downloading the profile image is handled in the ",(0,r.jsx)(t.code,{children:"ProfileHeader.tsx"})," and ",(0,r.jsx)(t.code,{children:"requests/profile.ts"})," files."]}),"\n",(0,r.jsxs)(t.p,{children:["Open the latter and implement the ",(0,r.jsx)(t.code,{children:"updateProfile"})," function."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import axios from 'axios';\n\nexport async function updateProfile(username: string, avatar: File|null): Promise {\n const formData = new FormData();\n formData.set('name', username);\n if (avatar) {\n formData.set('avatar', avatar);\n }\n\n await axios.post('/api/profile', formData, {\n headers: {\n 'content-type': 'multipart/form-data'\n }\n });\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Now, if you go back to ",(0,r.jsx)(t.a,{href:"http://localhost:3000/profile",children:"http://localhost:3000/profile"}),", you should be able to upload your profile picture."]})]})}function h(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},97529:(e,t,n)=>{n.d(t,{A:()=>r});const r=n.p+"assets/files/default-9490f4915bf9aca8c77fd98e411f2e2c.png"},28453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>o});var r=n(96540);const a={},i=r.createContext(a);function s(e){const t=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6a922744.00c5fe35.js b/assets/js/6a922744.63931c31.js similarity index 51% rename from assets/js/6a922744.00c5fe35.js rename to assets/js/6a922744.63931c31.js index ee7846b9b4..86b986e0a6 100644 --- a/assets/js/6a922744.00c5fe35.js +++ b/assets/js/6a922744.63931c31.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2252],{38547:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>i,contentTitle:()=>s,default:()=>d,frontMatter:()=>a,metadata:()=>l,toc:()=>c});var n=r(74848),o=r(28453);const a={title:"API Testing with Swagger",id:"tuto-6-swagger-interface",slug:"6-swagger-interface"},s=void 0,l={id:"tutorials/real-world-example-with-react/tuto-6-swagger-interface",title:"API Testing with Swagger",description:"Now that the first API endpoint has been implemented, the next step is to test it.",source:"@site/docs/tutorials/real-world-example-with-react/6-swagger-interface.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/6-swagger-interface",permalink:"/docs/tutorials/real-world-example-with-react/6-swagger-interface",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/6-swagger-interface.md",tags:[],version:"current",sidebarPosition:6,frontMatter:{title:"API Testing with Swagger",id:"tuto-6-swagger-interface",slug:"6-swagger-interface"},sidebar:"someSidebar",previous:{title:"Your First Route",permalink:"/docs/tutorials/real-world-example-with-react/5-our-first-route"},next:{title:"The Frontend App",permalink:"/docs/tutorials/real-world-example-with-react/7-add-frontend"}},i={},c=[];function p(e){const t={a:"a",code:"code",em:"em",img:"img",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:"Now that the first API endpoint has been implemented, the next step is to test it."}),"\n",(0,n.jsxs)(t.p,{children:["To do this, you will generate a complete documentation page of your API from which you can send requests. This page will be generated using ",(0,n.jsx)(t.a,{href:"https://swagger.io/tools/swagger-ui/",children:"Swagger UI"})," and the ",(0,n.jsx)(t.a,{href:"https://github.com/OAI/OpenAPI-Specification/",children:"OpenAPI specification"}),"."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"npm install @foal/swagger\n"})}),"\n",(0,n.jsx)(t.p,{children:"First, provide some information to describe your API globally."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { ApiInfo, ApiServer, controller } from '@foal/core';\nimport { StoriesController } from './api';\n\n@ApiInfo({\n title: 'Application API',\n version: '1.0.0'\n})\n@ApiServer({\n url: '/api'\n})\nexport class ApiController {\n\n subControllers = [\n controller('/stories', StoriesController),\n ];\n\n}\n\n"})}),"\n",(0,n.jsx)(t.p,{children:"Then generate a new controller. This one will be in charge of rendering the new page."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"foal generate controller openapi --register\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Make the generated class extend the abstract class ",(0,n.jsx)(t.code,{children:"SwaggerController"}),". And then provide the root controller of your API as an option to the controller."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\nimport { ApiController } from './api.controller';\n\nexport class OpenapiController extends SwaggerController {\n\n options = {\n controllerClass: ApiController\n }\n\n}\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Finally, update your ",(0,n.jsx)(t.code,{children:"app.controller.ts"})," file so that the Swagger UI page is available at ",(0,n.jsx)(t.a,{href:"http://localhost:3001/swagger",children:"/swagger"}),"."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, OpenapiController } from './controllers';\n\nexport class AppController implements IAppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenapiController)\n ];\n}\n\n"})}),"\n",(0,n.jsxs)(t.p,{children:["If you navigate to ",(0,n.jsx)(t.a,{href:"http://localhost:3001/swagger",children:"http://localhost:3001/swagger"}),", you will see the documentation page generated from your code."]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Swagger page",src:r(46334).A+"",width:"2560",height:"1394"})}),"\n",(0,n.jsxs)(t.p,{children:["Now click on the ",(0,n.jsx)(t.em,{children:"Try it out"})," button. The fields become editable and you can send requests to test your API."]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Swagger page",src:r(13685).A+"",width:"2560",height:"1386"})})]})}function d(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(p,{...e})}):p(e)}},46334:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/swagger1-3abe32cd345086f35f191e1284daa45e.png"},13685:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/swagger2-a7c73effa7473d646ed981f1f55acd4f.png"},28453:(e,t,r)=>{r.d(t,{R:()=>s,x:()=>l});var n=r(96540);const o={},a=n.createContext(o);function s(e){const t=n.useContext(a);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),n.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2252],{38547:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>i,contentTitle:()=>s,default:()=>d,frontMatter:()=>a,metadata:()=>l,toc:()=>c});var n=r(74848),o=r(28453);const a={title:"API Testing with Swagger",id:"tuto-6-swagger-interface",slug:"6-swagger-interface"},s=void 0,l={id:"tutorials/real-world-example-with-react/tuto-6-swagger-interface",title:"API Testing with Swagger",description:"Now that the first API endpoint has been implemented, the next step is to test it.",source:"@site/docs/tutorials/real-world-example-with-react/6-swagger-interface.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/6-swagger-interface",permalink:"/docs/tutorials/real-world-example-with-react/6-swagger-interface",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/6-swagger-interface.md",tags:[],version:"current",sidebarPosition:6,frontMatter:{title:"API Testing with Swagger",id:"tuto-6-swagger-interface",slug:"6-swagger-interface"},sidebar:"someSidebar",previous:{title:"Your First Route",permalink:"/docs/tutorials/real-world-example-with-react/5-our-first-route"},next:{title:"The Frontend App",permalink:"/docs/tutorials/real-world-example-with-react/7-add-frontend"}},i={},c=[];function p(e){const t={a:"a",code:"code",em:"em",img:"img",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:"Now that the first API endpoint has been implemented, the next step is to test it."}),"\n",(0,n.jsxs)(t.p,{children:["To do this, you will generate a complete documentation page of your API from which you can send requests. This page will be generated using ",(0,n.jsx)(t.a,{href:"https://swagger.io/tools/swagger-ui/",children:"Swagger UI"})," and the ",(0,n.jsx)(t.a,{href:"https://github.com/OAI/OpenAPI-Specification/",children:"OpenAPI specification"}),"."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"npm install @foal/swagger\n"})}),"\n",(0,n.jsx)(t.p,{children:"First, provide some information to describe your API globally."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { ApiInfo, ApiServer, controller } from '@foal/core';\nimport { StoriesController } from './api';\n\n@ApiInfo({\n title: 'Application API',\n version: '1.0.0'\n})\n@ApiServer({\n url: '/api'\n})\nexport class ApiController {\n\n subControllers = [\n controller('/stories', StoriesController),\n ];\n\n}\n\n"})}),"\n",(0,n.jsx)(t.p,{children:"Then generate a new controller. This one will be in charge of rendering the new page."}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"npx foal generate controller openapi --register\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Make the generated class extend the abstract class ",(0,n.jsx)(t.code,{children:"SwaggerController"}),". And then provide the root controller of your API as an option to the controller."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { SwaggerController } from '@foal/swagger';\nimport { ApiController } from './api.controller';\n\nexport class OpenapiController extends SwaggerController {\n\n options = {\n controllerClass: ApiController\n }\n\n}\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Finally, update your ",(0,n.jsx)(t.code,{children:"app.controller.ts"})," file so that the Swagger UI page is available at ",(0,n.jsx)(t.a,{href:"http://localhost:3001/swagger",children:"/swagger"}),"."]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, OpenapiController } from './controllers';\n\nexport class AppController implements IAppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenapiController)\n ];\n}\n\n"})}),"\n",(0,n.jsxs)(t.p,{children:["If you navigate to ",(0,n.jsx)(t.a,{href:"http://localhost:3001/swagger",children:"http://localhost:3001/swagger"}),", you will see the documentation page generated from your code."]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Swagger page",src:r(46334).A+"",width:"2560",height:"1394"})}),"\n",(0,n.jsxs)(t.p,{children:["Now click on the ",(0,n.jsx)(t.em,{children:"Try it out"})," button. The fields become editable and you can send requests to test your API."]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Swagger page",src:r(13685).A+"",width:"2560",height:"1386"})})]})}function d(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(p,{...e})}):p(e)}},46334:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/swagger1-3abe32cd345086f35f191e1284daa45e.png"},13685:(e,t,r)=>{r.d(t,{A:()=>n});const n=r.p+"assets/images/swagger2-a7c73effa7473d646ed981f1f55acd4f.png"},28453:(e,t,r)=>{r.d(t,{R:()=>s,x:()=>l});var n=r(96540);const o={},a=n.createContext(o);function s(e){const t=n.useContext(a);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),n.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6acfb0dd.074b8902.js b/assets/js/6acfb0dd.074b8902.js new file mode 100644 index 0000000000..48b7777a17 --- /dev/null +++ b/assets/js/6acfb0dd.074b8902.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2077],{32993:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>a,contentTitle:()=>t,default:()=>h,frontMatter:()=>r,metadata:()=>i,toc:()=>l});var c=s(74848),o=s(28453);const r={title:"Async tasks"},t=void 0,i={id:"common/async-tasks",title:"Async tasks",description:"Running an asynchronous task",source:"@site/docs/common/async-tasks.md",sourceDirName:"common",slug:"/common/async-tasks",permalink:"/docs/common/async-tasks",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/async-tasks.md",tags:[],version:"current",frontMatter:{title:"Async tasks"},sidebar:"someSidebar",previous:{title:"Logging",permalink:"/docs/common/logging"},next:{title:"REST API",permalink:"/docs/common/rest-blueprints"}},a={},l=[{value:"Running an asynchronous task",id:"running-an-asynchronous-task",level:2},{value:"Scheduling a job",id:"scheduling-a-job",level:2},{value:"Example",id:"example",level:3},{value:"Background Jobs with pm2",id:"background-jobs-with-pm2",level:3}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(n.h2,{id:"running-an-asynchronous-task",children:"Running an asynchronous task"}),"\n",(0,c.jsx)(n.p,{children:"In some situations, we need to execute a specific task without waiting for it and without blocking the request."}),"\n",(0,c.jsx)(n.p,{children:"This could be, for example, sending a specific message to the CRM or company chat. In this case, the user needs to be able to see his or her request completed as quickly as possible, even if the request to the CRM takes some time or fails."}),"\n",(0,c.jsxs)(n.p,{children:["To this end, Foal provides an ",(0,c.jsx)(n.code,{children:"AsyncService"})," to execute these tasks asynchronously, and correctly catch and log their errors where appropriate."]}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-typescript",children:"import { AsyncService, dependency } from '@foal/core';\n\nimport { CRMService } from './somewhere';\n\nexport class SubscriptionService {\n @dependency\n asyncService: AsyncService;\n\n @dependency\n crmService: CRMService;\n\n async subscribe(userId: number): Promise {\n // Do something\n\n this.asyncService.run(() => this.crmService.updateUser(userId));\n }\n}\n\n"})}),"\n",(0,c.jsx)(n.h2,{id:"scheduling-a-job",children:"Scheduling a job"}),"\n",(0,c.jsxs)(n.p,{children:["You can schedule jobs using ",(0,c.jsx)(n.a,{href:"/docs/cli/shell-scripts",children:"shell scripts"})," and the ",(0,c.jsx)(n.a,{href:"https://www.npmjs.com/package/node-schedule",children:"node-schedule"})," library."]}),"\n",(0,c.jsx)(n.h3,{id:"example",children:"Example"}),"\n",(0,c.jsx)(n.p,{children:(0,c.jsx)(n.em,{children:"scripts/fetch-metrics.ts"})}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-typescript",children:"export function main(args: any) {\n // Do some stuff\n}\n\n"})}),"\n",(0,c.jsx)(n.p,{children:(0,c.jsx)(n.em,{children:"scripts/schedule-jobs.ts"})}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport { scheduleJob } from 'node-schedule';\nimport { main as fetchMetrics } from './fetch-metrics';\n\nexport async function main(args: any) {\n console.log('Scheduling the job...');\n\n // Run the fetch-metrics script every day at 10:00 AM.\n scheduleJob(\n { hour: 10, minute: 0 },\n () => fetchMetrics(args)\n );\n\n console.log('Job scheduled!');\n}\n\n"})}),"\n",(0,c.jsx)(n.p,{children:"Schedule the job(s):"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-sh",children:"npm run build\nnpx foal run schedule-jobs arg1=value1\n"})}),"\n",(0,c.jsx)(n.h3,{id:"background-jobs-with-pm2",children:"Background Jobs with pm2"}),"\n",(0,c.jsxs)(n.p,{children:["While the above command works, it does not run the scheduler and the jobs in the background. To do this, you can use ",(0,c.jsx)(n.a,{href:"http://pm2.keymetrics.io/",children:"pm2"}),", a popular process manager for Node.js."]}),"\n",(0,c.jsxs)(n.p,{children:["First you need to install ",(0,c.jsx)(n.em,{children:"locally"})," the Foal CLI:"]}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{children:"npm install @foal/cli\n"})}),"\n",(0,c.jsx)(n.p,{children:"Then you can run the scheduler like this:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-sh",children:"pm2 start ./node_modules/.bin/foal --name scheduler -- run schedule-jobs arg1=value1\n"})}),"\n",(0,c.jsx)(n.p,{children:"If everything works fine, you should see your scheduler running with this command:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-sh",children:"pm2 ls\n"})}),"\n",(0,c.jsx)(n.p,{children:"To display the logs of the scheduler and the jobs, use this one:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{children:"pm2 logs scheduler\n"})}),"\n",(0,c.jsx)(n.p,{children:"Eventually, to stop the scheduler and the jobs, you can use this command:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{children:"pm2 delete scheduler\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,c.jsx)(n,{...e,children:(0,c.jsx)(d,{...e})}):d(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>t,x:()=>i});var c=s(96540);const o={},r=c.createContext(o);function t(e){const n=c.useContext(r);return c.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:t(e.components),c.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/6c2963dd.b2f4708a.js b/assets/js/6c2963dd.14dd24c2.js similarity index 55% rename from assets/js/6c2963dd.b2f4708a.js rename to assets/js/6c2963dd.14dd24c2.js index a6fa372272..9b339188c3 100644 --- a/assets/js/6c2963dd.b2f4708a.js +++ b/assets/js/6c2963dd.14dd24c2.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1784],{16294:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>c});var r=s(74848),n=s(28453);const a={title:"The Shell Scripts",id:"tuto-4-the-shell-scripts",slug:"4-the-shell-scripts"},o=void 0,i={id:"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts",title:"The Shell Scripts",description:"Your models are ready to be used. As in the previous tutorial, you will use shell scripts to feed the database.",source:"@site/docs/tutorials/real-world-example-with-react/4-the-shell-scripts.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/4-the-shell-scripts",permalink:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/4-the-shell-scripts.md",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"The Shell Scripts",id:"tuto-4-the-shell-scripts",slug:"4-the-shell-scripts"},sidebar:"someSidebar",previous:{title:"The User and Story Models",permalink:"/docs/tutorials/real-world-example-with-react/3-the-models"},next:{title:"Your First Route",permalink:"/docs/tutorials/real-world-example-with-react/5-our-first-route"}},l={},c=[{value:"The create-user script",id:"the-create-user-script",level:2},{value:"The create-story script",id:"the-create-story-script",level:2}];function d(e){const t={blockquote:"blockquote",code:"code",h2:"h2",p:"p",pre:"pre",...(0,n.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.p,{children:"Your models are ready to be used. As in the previous tutorial, you will use shell scripts to feed the database."}),"\n",(0,r.jsxs)(t.h2,{id:"the-create-user-script",children:["The ",(0,r.jsx)(t.code,{children:"create-user"})," script"]}),"\n",(0,r.jsxs)(t.p,{children:["A script called ",(0,r.jsx)(t.code,{children:"create-user"})," already exists in the ",(0,r.jsx)(t.code,{children:"scripts"})," directory."]}),"\n",(0,r.jsx)(t.p,{children:"Open the file and replace its content with the following:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"// 3p\nimport { hashPassword } from '@foal/core';\n\n// App\nimport { User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email', maxLength: 255 },\n password: { type: 'string' },\n name: { type: 'string', maxLength: 255 },\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport async function main(args: { email: string, password: string, name?: string }) {\n const user = new User();\n user.email = args.email;\n user.password = await hashPassword(args.password);\n user.name = args.name ?? 'Unknown';\n user.avatar = '';\n\n await dataSource.initialize();\n\n try {\n console.log(await user.save());\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,r.jsx)(t.p,{children:"Some parts of this code should look familiar to you."}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"schema"})," object is used to validate the arguments typed on the command line. In this case, the script expects two mandatory parameters ",(0,r.jsx)(t.code,{children:"email"})," and ",(0,r.jsx)(t.code,{children:"password"})," and an optional ",(0,r.jsx)(t.code,{children:"name"}),". The ",(0,r.jsx)(t.code,{children:"format"})," property checks that the ",(0,r.jsx)(t.code,{children:"email"})," string is an email (presence of ",(0,r.jsx)(t.code,{children:"@"})," character, etc)."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"main"})," function is called after successful validation. It is divided into several parts. First, it creates a new user with the arguments specified in the command line. Then it establishes a connection to the database and saves the user."]}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"hashPassword"})," function is used to hash and salt passwords before storing them in the database. For security reasons, you should use this function before saving passwords."]}),"\n"]}),"\n",(0,r.jsx)(t.p,{children:"Build the script."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,r.jsx)(t.p,{children:"Then create two new users."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:'foal run create-user email="john@foalts.org" password="john_password" name="John"\nfoal run create-user email="mary@foalts.org" password="mary_password" name="Mary"\n'})}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsx)(t.p,{children:"If you try to re-run one of these commands, you'll get the MySQL error below as the email key is unique."}),"\n",(0,r.jsx)(t.p,{children:(0,r.jsx)(t.code,{children:"ER_DUP_ENTRY: Duplicate entry 'john@foalts.org' for key 'IDX_xxx'"})}),"\n"]}),"\n",(0,r.jsxs)(t.h2,{id:"the-create-story-script",children:["The ",(0,r.jsx)(t.code,{children:"create-story"})," script"]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"create-story"})," script is a bit more complex as ",(0,r.jsx)(t.code,{children:"Story"})," has a many-to-one relation with ",(0,r.jsx)(t.code,{children:"User"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"foal generate script create-story\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Open the ",(0,r.jsx)(t.code,{children:"create-story.ts"})," file and replace its content."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { Story, User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n author: { type: 'string', format: 'email', maxLength: 255 },\n title: { type: 'string', maxLength: 255 },\n link: { type: 'string', maxLength: 255 },\n },\n required: [ 'author', 'title', 'link' ],\n type: 'object',\n};\n\nexport async function main(args: { author: string, title: string, link: string }) {\n await dataSource.initialize();\n\n const user = await User.findOneByOrFail({ email: args.author });\n\n const story = new Story();\n story.author = user;\n story.title = args.title;\n story.link = args.link;\n\n try {\n console.log(await story.save());\n } catch (error: any) {\n console.error(error);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,r.jsxs)(t.p,{children:["We added an ",(0,r.jsx)(t.code,{children:"author"})," parameter to know which user posted the story. It expects the user's email."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"main"})," function then tries to find the user who has this email. If it exists, the user is added to the story as the author. If it does not, then the script ends with a message displayed in the console."]}),"\n",(0,r.jsx)(t.p,{children:"Build the script."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,r.jsx)(t.p,{children:"And create new stories for each user."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:'foal run create-story author="john@foalts.org" title="How to build a simple to-do list" link="https://foalts.org/docs/tutorials/simple-todo-list/1-installation"\nfoal run create-story author="mary@foalts.org" title="FoalTS architecture overview" link="https://foalts.org/docs/architecture/architecture-overview"\nfoal run create-story author="mary@foalts.org" title="Authentication with Foal" link="https://foalts.org/docs/authentication/quick-start"\n'})})]})}function h(e={}){const{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},28453:(e,t,s)=>{s.d(t,{R:()=>o,x:()=>i});var r=s(96540);const n={},a=r.createContext(n);function o(e){const t=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:o(e.components),r.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1784],{16294:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>c});var r=s(74848),n=s(28453);const a={title:"The Shell Scripts",id:"tuto-4-the-shell-scripts",slug:"4-the-shell-scripts"},o=void 0,i={id:"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts",title:"The Shell Scripts",description:"Your models are ready to be used. As in the previous tutorial, you will use shell scripts to feed the database.",source:"@site/docs/tutorials/real-world-example-with-react/4-the-shell-scripts.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/4-the-shell-scripts",permalink:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/4-the-shell-scripts.md",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"The Shell Scripts",id:"tuto-4-the-shell-scripts",slug:"4-the-shell-scripts"},sidebar:"someSidebar",previous:{title:"The User and Story Models",permalink:"/docs/tutorials/real-world-example-with-react/3-the-models"},next:{title:"Your First Route",permalink:"/docs/tutorials/real-world-example-with-react/5-our-first-route"}},l={},c=[{value:"The create-user script",id:"the-create-user-script",level:2},{value:"The create-story script",id:"the-create-story-script",level:2}];function d(e){const t={blockquote:"blockquote",code:"code",h2:"h2",p:"p",pre:"pre",...(0,n.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.p,{children:"Your models are ready to be used. As in the previous tutorial, you will use shell scripts to feed the database."}),"\n",(0,r.jsxs)(t.h2,{id:"the-create-user-script",children:["The ",(0,r.jsx)(t.code,{children:"create-user"})," script"]}),"\n",(0,r.jsxs)(t.p,{children:["A script called ",(0,r.jsx)(t.code,{children:"create-user"})," already exists in the ",(0,r.jsx)(t.code,{children:"scripts"})," directory."]}),"\n",(0,r.jsx)(t.p,{children:"Open the file and replace its content with the following:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"// 3p\nimport { hashPassword } from '@foal/core';\n\n// App\nimport { User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email', maxLength: 255 },\n password: { type: 'string' },\n name: { type: 'string', maxLength: 255 },\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport async function main(args: { email: string, password: string, name?: string }) {\n const user = new User();\n user.email = args.email;\n user.password = await hashPassword(args.password);\n user.name = args.name ?? 'Unknown';\n user.avatar = '';\n\n await dataSource.initialize();\n\n try {\n console.log(await user.save());\n } catch (error: any) {\n console.log(error.message);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,r.jsx)(t.p,{children:"Some parts of this code should look familiar to you."}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"schema"})," object is used to validate the arguments typed on the command line. In this case, the script expects two mandatory parameters ",(0,r.jsx)(t.code,{children:"email"})," and ",(0,r.jsx)(t.code,{children:"password"})," and an optional ",(0,r.jsx)(t.code,{children:"name"}),". The ",(0,r.jsx)(t.code,{children:"format"})," property checks that the ",(0,r.jsx)(t.code,{children:"email"})," string is an email (presence of ",(0,r.jsx)(t.code,{children:"@"})," character, etc)."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"main"})," function is called after successful validation. It is divided into several parts. First, it creates a new user with the arguments specified in the command line. Then it establishes a connection to the database and saves the user."]}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"hashPassword"})," function is used to hash and salt passwords before storing them in the database. For security reasons, you should use this function before saving passwords."]}),"\n"]}),"\n",(0,r.jsx)(t.p,{children:"Build the script."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,r.jsx)(t.p,{children:"Then create two new users."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:'npx foal run create-user email="john@foalts.org" password="john_password" name="John"\nnpx foal run create-user email="mary@foalts.org" password="mary_password" name="Mary"\n'})}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsx)(t.p,{children:"If you try to re-run one of these commands, you'll get the MySQL error below as the email key is unique."}),"\n",(0,r.jsx)(t.p,{children:(0,r.jsx)(t.code,{children:"ER_DUP_ENTRY: Duplicate entry 'john@foalts.org' for key 'IDX_xxx'"})}),"\n"]}),"\n",(0,r.jsxs)(t.h2,{id:"the-create-story-script",children:["The ",(0,r.jsx)(t.code,{children:"create-story"})," script"]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"create-story"})," script is a bit more complex as ",(0,r.jsx)(t.code,{children:"Story"})," has a many-to-one relation with ",(0,r.jsx)(t.code,{children:"User"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npx foal generate script create-story\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Open the ",(0,r.jsx)(t.code,{children:"create-story.ts"})," file and replace its content."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { Story, User } from '../app/entities';\nimport { dataSource } from '../db';\n\nexport const schema = {\n additionalProperties: false,\n properties: {\n author: { type: 'string', format: 'email', maxLength: 255 },\n title: { type: 'string', maxLength: 255 },\n link: { type: 'string', maxLength: 255 },\n },\n required: [ 'author', 'title', 'link' ],\n type: 'object',\n};\n\nexport async function main(args: { author: string, title: string, link: string }) {\n await dataSource.initialize();\n\n const user = await User.findOneByOrFail({ email: args.author });\n\n const story = new Story();\n story.author = user;\n story.title = args.title;\n story.link = args.link;\n\n try {\n console.log(await story.save());\n } catch (error: any) {\n console.error(error);\n } finally {\n await dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,r.jsxs)(t.p,{children:["We added an ",(0,r.jsx)(t.code,{children:"author"})," parameter to know which user posted the story. It expects the user's email."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"main"})," function then tries to find the user who has this email. If it exists, the user is added to the story as the author. If it does not, then the script ends with a message displayed in the console."]}),"\n",(0,r.jsx)(t.p,{children:"Build the script."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,r.jsx)(t.p,{children:"And create new stories for each user."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:'npx foal run create-story author="john@foalts.org" title="How to build a simple to-do list" link="https://foalts.org/docs/tutorials/simple-todo-list/1-installation"\nnpx foal run create-story author="mary@foalts.org" title="FoalTS architecture overview" link="https://foalts.org/docs/architecture/architecture-overview"\nnpx foal run create-story author="mary@foalts.org" title="Authentication with Foal" link="https://foalts.org/docs/authentication/quick-start"\n'})})]})}function h(e={}){const{wrapper:t}={...(0,n.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},28453:(e,t,s)=>{s.d(t,{R:()=>o,x:()=>i});var r=s(96540);const n={},a=r.createContext(n);function o(e){const t=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:o(e.components),r.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/734aee64.1f52650f.js b/assets/js/734aee64.1f52650f.js deleted file mode 100644 index 333657770b..0000000000 --- a/assets/js/734aee64.1f52650f.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[972],{77322:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>l,toc:()=>u});var t=r(74848),s=r(28453),c=r(11470),i=r(19365);const a={title:"Services & Dependency Injection"},o=void 0,l={id:"architecture/services-and-dependency-injection",title:"Services & Dependency Injection",description:"Description",source:"@site/docs/architecture/services-and-dependency-injection.md",sourceDirName:"architecture",slug:"/architecture/services-and-dependency-injection",permalink:"/docs/architecture/services-and-dependency-injection",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/services-and-dependency-injection.md",tags:[],version:"current",frontMatter:{title:"Services & Dependency Injection"},sidebar:"someSidebar",previous:{title:"Controllers",permalink:"/docs/architecture/controllers"},next:{title:"Hooks",permalink:"/docs/architecture/hooks"}},d={},u=[{value:"Description",id:"description",level:2},{value:"Architecture",id:"architecture",level:2},{value:"Use & Dependency Injection",id:"use--dependency-injection",level:2},{value:"Testing services",id:"testing-services",level:2},{value:"Services (or Controllers) with Dependencies",id:"services-or-controllers-with-dependencies",level:3},{value:"Injecting other Instances",id:"injecting-other-instances",level:2},{value:"Abstract Services",id:"abstract-services",level:2},{value:"Default Concrete Services",id:"default-concrete-services",level:3},{value:"Usage with Interfaces and Generic Classes",id:"usage-with-interfaces-and-generic-classes",level:2},{value:"Accessing the ServiceManager",id:"accessing-the-servicemanager",level:2}];function p(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"foal generate service my-service\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class MyService {\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"description",children:"Description"}),"\n",(0,t.jsx)(n.p,{children:"Services are useful to organize your code in domains. They can be used in a wide variety of situations: logging, interaction with a database, calculations, communication with an external API, etc."}),"\n",(0,t.jsx)(n.h2,{id:"architecture",children:"Architecture"}),"\n",(0,t.jsx)(n.p,{children:"Basically, a service can be any class with a narrow and well defined purpose. They are instantiated as singletons."}),"\n",(0,t.jsx)(n.h2,{id:"use--dependency-injection",children:"Use & Dependency Injection"}),"\n",(0,t.jsxs)(n.p,{children:["You can access a service from a controller using the ",(0,t.jsx)(n.code,{children:"@dependency"})," decorator."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Get, HttpResponseOK } from '@foal/core';\n\nclass Logger {\n log(message: string) {\n console.log(`${new Date()} - ${message}`);\n }\n}\n\nclass AppController {\n @dependency\n logger: Logger\n\n @Get('/')\n index() {\n this.logger.log('index has been called!');\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["When instantiating the controller, FoalTS will provide the service instance. This mechanism is called ",(0,t.jsx)(n.em,{children:"dependency injection"})," and is particularly interesting in unit testing (see section below)."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In the same way, you can access a service from another service."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency } from '@foal/core';\n\nclass MyService {\n run() {\n console.log('hello world');\n }\n}\n\nclass MyServiceA {\n @dependency\n myService: MyService;\n\n foo() {\n this.myService.run();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Dependencies are injected after the instantiation of the controller/service. So they will appear as ",(0,t.jsx)(n.code,{children:"undefined"})," if you try to read them inside a constructor. If you want to access the dependencies when initializing a controller/service, refer to the ",(0,t.jsxs)(n.a,{href:"/docs/architecture/initialization",children:[(0,t.jsx)(n.code,{children:"boot"})," method"]}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"Circular dependencies are not supported. In most cases, when two services are dependent on each other, the creation of a third service containing the functionalities required by both services solves the dependency problem."}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"testing-services",children:"Testing services"}),"\n",(0,t.jsx)(n.p,{children:"Services are classes and so can be tested as is."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// calculator.service.ts\nexport class CalculatorService {\n sum(a: number, b: number): number {\n return a + b;\n }\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// calculator.service.spec.ts\nimport { strictEqual } from 'assert';\nimport { CalculatorService } from './calculator.service';\n\nit('CalculatorService', () => {\n const service = new CalculatorService();\n strictEqual(service.sum(1, 2), 3);\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"services-or-controllers-with-dependencies",children:"Services (or Controllers) with Dependencies"}),"\n",(0,t.jsxs)(n.p,{children:["If your service has dependencies, you can use the ",(0,t.jsx)(n.code,{children:"createService"})," function to instantiate the service with them."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// weather.service.ts\nimport { dependency } from '@foal/core';\n\nclass ConversionService {\n celsiusToFahrenheit(temperature: number): number {\n return temperature * 9 / 5 + 32;\n }\n}\n\nclass WeatherService {\n temp = 14;\n\n @dependency\n conversion: ConversionService;\n\n getWeather(): string {\n const temp = this.conversion.celsiusToFahrenheit(this.temp);\n return `The outside temperature is ${temp} \xb0F.`;\n }\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// weather.service.spec.ts\nimport { strictEqual } from 'assert';\nimport { createService } from '@foal/core';\nimport { WeatherService } from './weather.service';\n\nit('WeatherService', () => {\n const service = createService(WeatherService);\n\n const expected = 'The outside temperature is 57.2 \xb0F.';\n const actual = service.getWeather();\n\n strictEqual(actual, expected);\n});\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["A similar function exists to instantiate controllers with their dependencies: ",(0,t.jsx)(n.code,{children:"createController"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["In many situations, it is necessary to mock the dependencies to truly write ",(0,t.jsx)(n.em,{children:"unit"})," tests. This can be done by passing a second argument to ",(0,t.jsx)(n.code,{children:"createService"})," (or ",(0,t.jsx)(n.code,{children:"createController"}),")."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// detector.service.ts\nimport { dependency } from '@foal/core';\n\nclass TwitterService {\n fetchLastTweets(): { msg: string }[] {\n // Make a call to the Twitter API to get the last tweets.\n return [];\n }\n}\n\nclass DetectorService {\n @dependency\n twitter: TwitterService;\n\n isFoalTSMentionedInTheLastTweets() {\n const tweets = this.twitter.fetchLastTweets();\n if (tweets.find(tweet => tweet.msg.includes('FoalTS'))) {\n return true;\n }\n return false;\n }\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// detector.service.spec.ts\nimport { strictEqual } from 'assert';\nimport { createService } from '@foal/core';\nimport { DetectorService } from './weather.service';\n\nit('DetectorService', () => {\n const twitterMock = {\n fetchLastTweets() {\n return [\n { msg: 'Hello world!' },\n { msg: 'I LOVE FoalTS' },\n ]\n }\n }\n const service = createService(DetectorService, {\n twitter: twitterMock\n });\n\n const actual = service.isFoalTSMentionedInTheLastTweets();\n\n strictEqual(actual, true);\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"injecting-other-instances",children:"Injecting other Instances"}),"\n",(0,t.jsxs)(n.p,{children:["To manually inject instances into the identity mapper, you can also provide your own ",(0,t.jsx)(n.code,{children:"ServiceManager"})," to the ",(0,t.jsx)(n.code,{children:"createApp"})," function (usually located at ",(0,t.jsx)(n.code,{children:"src/index.ts"}),")."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/index.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { DataSource } from 'typeorm';\n\nimport { AppController } from './app/app.controller';\nimport { dataSource } from './db';\n\nasync function main() {\n await dataSource.initialize();\n\n const serviceManager = new ServiceManager();\n serviceManager.set(DataSource, dataSource);\n\n const app = await createApp(AppController, {\n serviceManager\n });\n\n // ...\n}\n\n// ...\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Note: Interfaces cannot be passed to the ",(0,t.jsx)(n.code,{children:"set"})," method."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/controllers/api.controller.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Get, HttpResponseOK } from '@foal/core';\nimport { DataSource } from 'typeorm';\n\nimport { Product } from '../entities';\n\nclass ApiController {\n\n @dependency\n dataSource: DataSource;\n\n @Get('/products')\n async readProducts() {\n const products = await this.dataSource.getRepository(Product).find();\n return new HttpResponseOK(products);\n }\n\n}\n\n"})}),"\n",(0,t.jsx)(n.h2,{id:"abstract-services",children:"Abstract Services"}),"\n",(0,t.jsx)(n.p,{children:"If you want to use a different service implementation depending on your environment (production, development, etc.), you can use an abstract service for this."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"logger.service.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export abstract class Logger {\n static concreteClassConfigPath = 'logger.driver';\n static concreteClassName = 'ConcreteLogger';\n\n abstract log(str: string): void;\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Warning:"})," the two properties must be static."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"console-logger.service.ts (concrete service)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class ConsoleLogger extends Logger {\n log(str: string) {\n console.log(str);\n }\n}\n\nexport { ConsoleLogger as ConcreteLogger };\n"})}),"\n",(0,t.jsxs)(c.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"logger:\n driver: ./app/services/console-logger.service\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "logger": {\n "driver": "./app/services/console-logger.service"\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n logger: {\n driver: "./app/services/console-logger.service"\n }\n}\n'})})})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The configuration value can be a package name or a path relative to the ",(0,t.jsx)(n.code,{children:"src/"})," directory. If it is a path, it ",(0,t.jsx)(n.strong,{children:"must"})," start with ",(0,t.jsx)(n.code,{children:"./"})," and ",(0,t.jsx)(n.strong,{children:"must not"})," have an extension (",(0,t.jsx)(n.code,{children:".js"}),", ",(0,t.jsx)(n.code,{children:".ts"}),", etc)."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"a random service"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class Service {\n @dependency\n logger: Logger;\n\n // ...\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"default-concrete-services",children:"Default Concrete Services"}),"\n",(0,t.jsxs)(n.p,{children:["An abstract service can have a default concrete service that is used when no configuration value is specified or when the configuration value is ",(0,t.jsx)(n.code,{children:"local"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { join } from 'path';\n\nexport abstract class Logger {\n static concreteClassConfigPath = 'logger.driver';\n static concreteClassName = 'ConcreteLogger';\n static defaultConcreteClassPath = join(__dirname, './console-logger.service');\n\n abstract log(str: string): void;\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"usage-with-interfaces-and-generic-classes",children:"Usage with Interfaces and Generic Classes"}),"\n",(0,t.jsxs)(n.p,{children:["Interfaces and generic classes can be injected using strings as IDs. To do this, you will need the ",(0,t.jsx)(n.code,{children:"@Dependency"})," decorator."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/services/logger.interface.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export interface ILogger {\n log(message: any): void;\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/services/logger.service.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ILogger } from './logger.interface';\n\nexport class ConsoleLogger implements ILogger {\n log(message: any): void {\n console.log(message);\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/index.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\n\nimport { AppController } from './app/app.controller';\nimport { Product } from './app/entities';\nimport { ConsoleLogger } from './app/services';\nimport { dataSource } from './db';\n\nasync function main() {\n await dataSource.initialize();\n const productRepository = dataSource.getRepository(Product);\n\n const serviceManager = new ServiceManager()\n .set('product', productRepository)\n .set('logger', new ConsoleLogger());\n\n const app = await createApp(AppController, {\n serviceManager\n });\n\n // ...\n}\n\n// ...\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/controllers/api.controller.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Dependency, Get, HttpResponseOK } from '@foal/core';\nimport { Repository } from 'typeorm';\n\nimport { Product } from '../entities';\nimport { ILogger } from '../services';\n\nexport class ApiController {\n\n @Dependency('product')\n productRepository: Repository;\n\n @Dependency('logger')\n logger: ILogger;\n\n @Get('/products')\n async readProducts()\xa0{\n const products = await this.productRepository.find();\n this.logger.log(products);\n return new HttpResponseOK(products);\n }\n\n}\n\n"})}),"\n",(0,t.jsxs)(n.h2,{id:"accessing-the-servicemanager",children:["Accessing the ",(0,t.jsx)(n.code,{children:"ServiceManager"})]}),"\n",(0,t.jsxs)(n.p,{children:["In very rare situations, you may want to access the ",(0,t.jsx)(n.code,{children:"ServiceManager"})," which is the identity mapper that contains all the service instances."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Get, HttpResponseOK, ServiceManager } from '@foal/core';\n\nclass MyService {\n foo() {\n return 'foo';\n }\n}\n\nclass MyController {\n @dependency\n services: ServiceManager;\n\n @Get('/bar')\n bar() {\n const msg = this.services.get(MyService).foo();\n return new HttpResponseOK(msg);\n }\n}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(p,{...e})}):p(e)}},19365:(e,n,r)=>{r.d(n,{A:()=>i});r(96540);var t=r(34164);const s={tabItem:"tabItem_Ymn6"};var c=r(74848);function i(e){let{children:n,hidden:r,className:i}=e;return(0,c.jsx)("div",{role:"tabpanel",className:(0,t.A)(s.tabItem,i),hidden:r,children:n})}},11470:(e,n,r)=>{r.d(n,{A:()=>w});var t=r(96540),s=r(34164),c=r(23104),i=r(56347),a=r(205),o=r(57485),l=r(31682),d=r(89466);function u(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:r}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:r,attributes:t,default:s}}=e;return{value:n,label:r,attributes:t,default:s}}))}(r);return function(e){const n=(0,l.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,r])}function h(e){let{value:n,tabValues:r}=e;return r.some((e=>e.value===n))}function g(e){let{queryString:n=!1,groupId:r}=e;const s=(0,i.W6)(),c=function(e){let{queryString:n=!1,groupId:r}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:n,groupId:r});return[(0,o.aZ)(c),(0,t.useCallback)((e=>{if(!c)return;const n=new URLSearchParams(s.location.search);n.set(c,e),s.replace({...s.location,search:n.toString()})}),[c,s])]}function m(e){const{defaultValue:n,queryString:r=!1,groupId:s}=e,c=p(e),[i,o]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!h({value:n,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=r.find((e=>e.default))??r[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:c}))),[l,u]=g({queryString:r,groupId:s}),[m,v]=function(e){let{groupId:n}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,c]=(0,d.Dv)(r);return[s,(0,t.useCallback)((e=>{r&&c.set(e)}),[r,c])]}({groupId:s}),x=(()=>{const e=l??m;return h({value:e,tabValues:c})?e:null})();(0,a.A)((()=>{x&&o(x)}),[x]);return{selectedValue:i,selectValue:(0,t.useCallback)((e=>{if(!h({value:e,tabValues:c}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),v(e)}),[u,v,c]),tabValues:c}}var v=r(92303);const x={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=r(74848);function f(e){let{className:n,block:r,selectedValue:t,selectValue:i,tabValues:a}=e;const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,c.a_)(),d=e=>{const n=e.currentTarget,r=o.indexOf(n),s=a[r].value;s!==t&&(l(n),i(s))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=o.indexOf(e.currentTarget)+1;n=o[r]??o[0];break}case"ArrowLeft":{const r=o.indexOf(e.currentTarget)-1;n=o[r]??o[o.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":r},n),children:a.map((e=>{let{value:n,label:r,attributes:c}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>o.push(e),onKeyDown:u,onClick:d,...c,className:(0,s.A)("tabs__item",x.tabItem,c?.className,{"tabs__item--active":t===n}),children:r??n},n)}))})}function y(e){let{lazy:n,children:r,selectedValue:s}=e;const c=(Array.isArray(r)?r:[r]).filter(Boolean);if(n){const e=c.find((e=>e.props.value===s));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:c.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function b(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,s.A)("tabs-container",x.tabList),children:[(0,j.jsx)(f,{...e,...n}),(0,j.jsx)(y,{...e,...n})]})}function w(e){const n=(0,v.A)();return(0,j.jsx)(b,{...e,children:u(e.children)},String(n))}},28453:(e,n,r)=>{r.d(n,{R:()=>i,x:()=>a});var t=r(96540);const s={},c=t.createContext(s);function i(e){const n=t.useContext(c);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),t.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/734aee64.a30d90fa.js b/assets/js/734aee64.a30d90fa.js new file mode 100644 index 0000000000..88297f6101 --- /dev/null +++ b/assets/js/734aee64.a30d90fa.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[972],{77322:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>a,metadata:()=>l,toc:()=>u});var t=r(74848),s=r(28453),c=r(11470),i=r(19365);const a={title:"Services & Dependency Injection"},o=void 0,l={id:"architecture/services-and-dependency-injection",title:"Services & Dependency Injection",description:"Description",source:"@site/docs/architecture/services-and-dependency-injection.md",sourceDirName:"architecture",slug:"/architecture/services-and-dependency-injection",permalink:"/docs/architecture/services-and-dependency-injection",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/services-and-dependency-injection.md",tags:[],version:"current",frontMatter:{title:"Services & Dependency Injection"},sidebar:"someSidebar",previous:{title:"Controllers",permalink:"/docs/architecture/controllers"},next:{title:"Hooks",permalink:"/docs/architecture/hooks"}},d={},u=[{value:"Description",id:"description",level:2},{value:"Architecture",id:"architecture",level:2},{value:"Use & Dependency Injection",id:"use--dependency-injection",level:2},{value:"Testing services",id:"testing-services",level:2},{value:"Services (or Controllers) with Dependencies",id:"services-or-controllers-with-dependencies",level:3},{value:"Injecting other Instances",id:"injecting-other-instances",level:2},{value:"Abstract Services",id:"abstract-services",level:2},{value:"Default Concrete Services",id:"default-concrete-services",level:3},{value:"Usage with Interfaces and Generic Classes",id:"usage-with-interfaces-and-generic-classes",level:2},{value:"Accessing the ServiceManager",id:"accessing-the-servicemanager",level:2}];function p(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"npx foal generate service my-service\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class MyService {\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"description",children:"Description"}),"\n",(0,t.jsx)(n.p,{children:"Services are useful to organize your code in domains. They can be used in a wide variety of situations: logging, interaction with a database, calculations, communication with an external API, etc."}),"\n",(0,t.jsx)(n.h2,{id:"architecture",children:"Architecture"}),"\n",(0,t.jsx)(n.p,{children:"Basically, a service can be any class with a narrow and well defined purpose. They are instantiated as singletons."}),"\n",(0,t.jsx)(n.h2,{id:"use--dependency-injection",children:"Use & Dependency Injection"}),"\n",(0,t.jsxs)(n.p,{children:["You can access a service from a controller using the ",(0,t.jsx)(n.code,{children:"@dependency"})," decorator."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Get, HttpResponseOK } from '@foal/core';\n\nclass Logger {\n log(message: string) {\n console.log(`${new Date()} - ${message}`);\n }\n}\n\nclass AppController {\n @dependency\n logger: Logger\n\n @Get('/')\n index() {\n this.logger.log('index has been called!');\n return new HttpResponseOK('Hello world!');\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["When instantiating the controller, FoalTS will provide the service instance. This mechanism is called ",(0,t.jsx)(n.em,{children:"dependency injection"})," and is particularly interesting in unit testing (see section below)."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In the same way, you can access a service from another service."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency } from '@foal/core';\n\nclass MyService {\n run() {\n console.log('hello world');\n }\n}\n\nclass MyServiceA {\n @dependency\n myService: MyService;\n\n foo() {\n this.myService.run();\n }\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Dependencies are injected after the instantiation of the controller/service. So they will appear as ",(0,t.jsx)(n.code,{children:"undefined"})," if you try to read them inside a constructor. If you want to access the dependencies when initializing a controller/service, refer to the ",(0,t.jsxs)(n.a,{href:"/docs/architecture/initialization",children:[(0,t.jsx)(n.code,{children:"boot"})," method"]}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"Circular dependencies are not supported. In most cases, when two services are dependent on each other, the creation of a third service containing the functionalities required by both services solves the dependency problem."}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"testing-services",children:"Testing services"}),"\n",(0,t.jsx)(n.p,{children:"Services are classes and so can be tested as is."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// calculator.service.ts\nexport class CalculatorService {\n sum(a: number, b: number): number {\n return a + b;\n }\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// calculator.service.spec.ts\nimport { strictEqual } from 'assert';\nimport { CalculatorService } from './calculator.service';\n\nit('CalculatorService', () => {\n const service = new CalculatorService();\n strictEqual(service.sum(1, 2), 3);\n});\n"})}),"\n",(0,t.jsx)(n.h3,{id:"services-or-controllers-with-dependencies",children:"Services (or Controllers) with Dependencies"}),"\n",(0,t.jsxs)(n.p,{children:["If your service has dependencies, you can use the ",(0,t.jsx)(n.code,{children:"createService"})," function to instantiate the service with them."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// weather.service.ts\nimport { dependency } from '@foal/core';\n\nclass ConversionService {\n celsiusToFahrenheit(temperature: number): number {\n return temperature * 9 / 5 + 32;\n }\n}\n\nclass WeatherService {\n temp = 14;\n\n @dependency\n conversion: ConversionService;\n\n getWeather(): string {\n const temp = this.conversion.celsiusToFahrenheit(this.temp);\n return `The outside temperature is ${temp} \xb0F.`;\n }\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// weather.service.spec.ts\nimport { strictEqual } from 'assert';\nimport { createService } from '@foal/core';\nimport { WeatherService } from './weather.service';\n\nit('WeatherService', () => {\n const service = createService(WeatherService);\n\n const expected = 'The outside temperature is 57.2 \xb0F.';\n const actual = service.getWeather();\n\n strictEqual(actual, expected);\n});\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["A similar function exists to instantiate controllers with their dependencies: ",(0,t.jsx)(n.code,{children:"createController"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["In many situations, it is necessary to mock the dependencies to truly write ",(0,t.jsx)(n.em,{children:"unit"})," tests. This can be done by passing a second argument to ",(0,t.jsx)(n.code,{children:"createService"})," (or ",(0,t.jsx)(n.code,{children:"createController"}),")."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Example:"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// detector.service.ts\nimport { dependency } from '@foal/core';\n\nclass TwitterService {\n fetchLastTweets(): { msg: string }[] {\n // Make a call to the Twitter API to get the last tweets.\n return [];\n }\n}\n\nclass DetectorService {\n @dependency\n twitter: TwitterService;\n\n isFoalTSMentionedInTheLastTweets() {\n const tweets = this.twitter.fetchLastTweets();\n if (tweets.find(tweet => tweet.msg.includes('FoalTS'))) {\n return true;\n }\n return false;\n }\n}\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"// detector.service.spec.ts\nimport { strictEqual } from 'assert';\nimport { createService } from '@foal/core';\nimport { DetectorService } from './weather.service';\n\nit('DetectorService', () => {\n const twitterMock = {\n fetchLastTweets() {\n return [\n { msg: 'Hello world!' },\n { msg: 'I LOVE FoalTS' },\n ]\n }\n }\n const service = createService(DetectorService, {\n twitter: twitterMock\n });\n\n const actual = service.isFoalTSMentionedInTheLastTweets();\n\n strictEqual(actual, true);\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"injecting-other-instances",children:"Injecting other Instances"}),"\n",(0,t.jsxs)(n.p,{children:["To manually inject instances into the identity mapper, you can also provide your own ",(0,t.jsx)(n.code,{children:"ServiceManager"})," to the ",(0,t.jsx)(n.code,{children:"createApp"})," function (usually located at ",(0,t.jsx)(n.code,{children:"src/index.ts"}),")."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/index.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { DataSource } from 'typeorm';\n\nimport { AppController } from './app/app.controller';\nimport { dataSource } from './db';\n\nasync function main() {\n await dataSource.initialize();\n\n const serviceManager = new ServiceManager();\n serviceManager.set(DataSource, dataSource);\n\n const app = await createApp(AppController, {\n serviceManager\n });\n\n // ...\n}\n\n// ...\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Note: Interfaces cannot be passed to the ",(0,t.jsx)(n.code,{children:"set"})," method."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/controllers/api.controller.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Get, HttpResponseOK } from '@foal/core';\nimport { DataSource } from 'typeorm';\n\nimport { Product } from '../entities';\n\nclass ApiController {\n\n @dependency\n dataSource: DataSource;\n\n @Get('/products')\n async readProducts() {\n const products = await this.dataSource.getRepository(Product).find();\n return new HttpResponseOK(products);\n }\n\n}\n\n"})}),"\n",(0,t.jsx)(n.h2,{id:"abstract-services",children:"Abstract Services"}),"\n",(0,t.jsx)(n.p,{children:"If you want to use a different service implementation depending on your environment (production, development, etc.), you can use an abstract service for this."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"logger.service.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export abstract class Logger {\n static concreteClassConfigPath = 'logger.driver';\n static concreteClassName = 'ConcreteLogger';\n\n abstract log(str: string): void;\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Warning:"})," the two properties must be static."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"console-logger.service.ts (concrete service)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class ConsoleLogger extends Logger {\n log(str: string) {\n console.log(str);\n }\n}\n\nexport { ConsoleLogger as ConcreteLogger };\n"})}),"\n",(0,t.jsxs)(c.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"logger:\n driver: ./app/services/console-logger.service\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "logger": {\n "driver": "./app/services/console-logger.service"\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n logger: {\n driver: "./app/services/console-logger.service"\n }\n}\n'})})})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["The configuration value can be a package name or a path relative to the ",(0,t.jsx)(n.code,{children:"src/"})," directory. If it is a path, it ",(0,t.jsx)(n.strong,{children:"must"})," start with ",(0,t.jsx)(n.code,{children:"./"})," and ",(0,t.jsx)(n.strong,{children:"must not"})," have an extension (",(0,t.jsx)(n.code,{children:".js"}),", ",(0,t.jsx)(n.code,{children:".ts"}),", etc)."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"a random service"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class Service {\n @dependency\n logger: Logger;\n\n // ...\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"default-concrete-services",children:"Default Concrete Services"}),"\n",(0,t.jsxs)(n.p,{children:["An abstract service can have a default concrete service that is used when no configuration value is specified or when the configuration value is ",(0,t.jsx)(n.code,{children:"local"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { join } from 'path';\n\nexport abstract class Logger {\n static concreteClassConfigPath = 'logger.driver';\n static concreteClassName = 'ConcreteLogger';\n static defaultConcreteClassPath = join(__dirname, './console-logger.service');\n\n abstract log(str: string): void;\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"usage-with-interfaces-and-generic-classes",children:"Usage with Interfaces and Generic Classes"}),"\n",(0,t.jsxs)(n.p,{children:["Interfaces and generic classes can be injected using strings as IDs. To do this, you will need the ",(0,t.jsx)(n.code,{children:"@Dependency"})," decorator."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/services/logger.interface.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export interface ILogger {\n log(message: any): void;\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/services/logger.service.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { ILogger } from './logger.interface';\n\nexport class ConsoleLogger implements ILogger {\n log(message: any): void {\n console.log(message);\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/index.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\n\nimport { AppController } from './app/app.controller';\nimport { Product } from './app/entities';\nimport { ConsoleLogger } from './app/services';\nimport { dataSource } from './db';\n\nasync function main() {\n await dataSource.initialize();\n const productRepository = dataSource.getRepository(Product);\n\n const serviceManager = new ServiceManager()\n .set('product', productRepository)\n .set('logger', new ConsoleLogger());\n\n const app = await createApp(AppController, {\n serviceManager\n });\n\n // ...\n}\n\n// ...\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/controllers/api.controller.ts (example)"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Dependency, Get, HttpResponseOK } from '@foal/core';\nimport { Repository } from 'typeorm';\n\nimport { Product } from '../entities';\nimport { ILogger } from '../services';\n\nexport class ApiController {\n\n @Dependency('product')\n productRepository: Repository;\n\n @Dependency('logger')\n logger: ILogger;\n\n @Get('/products')\n async readProducts()\xa0{\n const products = await this.productRepository.find();\n this.logger.log(products);\n return new HttpResponseOK(products);\n }\n\n}\n\n"})}),"\n",(0,t.jsxs)(n.h2,{id:"accessing-the-servicemanager",children:["Accessing the ",(0,t.jsx)(n.code,{children:"ServiceManager"})]}),"\n",(0,t.jsxs)(n.p,{children:["In very rare situations, you may want to access the ",(0,t.jsx)(n.code,{children:"ServiceManager"})," which is the identity mapper that contains all the service instances."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { dependency, Get, HttpResponseOK, ServiceManager } from '@foal/core';\n\nclass MyService {\n foo() {\n return 'foo';\n }\n}\n\nclass MyController {\n @dependency\n services: ServiceManager;\n\n @Get('/bar')\n bar() {\n const msg = this.services.get(MyService).foo();\n return new HttpResponseOK(msg);\n }\n}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(p,{...e})}):p(e)}},19365:(e,n,r)=>{r.d(n,{A:()=>i});r(96540);var t=r(34164);const s={tabItem:"tabItem_Ymn6"};var c=r(74848);function i(e){let{children:n,hidden:r,className:i}=e;return(0,c.jsx)("div",{role:"tabpanel",className:(0,t.A)(s.tabItem,i),hidden:r,children:n})}},11470:(e,n,r)=>{r.d(n,{A:()=>w});var t=r(96540),s=r(34164),c=r(23104),i=r(56347),a=r(205),o=r(57485),l=r(31682),d=r(89466);function u(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:r}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:r,attributes:t,default:s}}=e;return{value:n,label:r,attributes:t,default:s}}))}(r);return function(e){const n=(0,l.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,r])}function h(e){let{value:n,tabValues:r}=e;return r.some((e=>e.value===n))}function g(e){let{queryString:n=!1,groupId:r}=e;const s=(0,i.W6)(),c=function(e){let{queryString:n=!1,groupId:r}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:n,groupId:r});return[(0,o.aZ)(c),(0,t.useCallback)((e=>{if(!c)return;const n=new URLSearchParams(s.location.search);n.set(c,e),s.replace({...s.location,search:n.toString()})}),[c,s])]}function m(e){const{defaultValue:n,queryString:r=!1,groupId:s}=e,c=p(e),[i,o]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!h({value:n,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=r.find((e=>e.default))??r[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:c}))),[l,u]=g({queryString:r,groupId:s}),[m,v]=function(e){let{groupId:n}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,c]=(0,d.Dv)(r);return[s,(0,t.useCallback)((e=>{r&&c.set(e)}),[r,c])]}({groupId:s}),x=(()=>{const e=l??m;return h({value:e,tabValues:c})?e:null})();(0,a.A)((()=>{x&&o(x)}),[x]);return{selectedValue:i,selectValue:(0,t.useCallback)((e=>{if(!h({value:e,tabValues:c}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),v(e)}),[u,v,c]),tabValues:c}}var v=r(92303);const x={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=r(74848);function f(e){let{className:n,block:r,selectedValue:t,selectValue:i,tabValues:a}=e;const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,c.a_)(),d=e=>{const n=e.currentTarget,r=o.indexOf(n),s=a[r].value;s!==t&&(l(n),i(s))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=o.indexOf(e.currentTarget)+1;n=o[r]??o[0];break}case"ArrowLeft":{const r=o.indexOf(e.currentTarget)-1;n=o[r]??o[o.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":r},n),children:a.map((e=>{let{value:n,label:r,attributes:c}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>o.push(e),onKeyDown:u,onClick:d,...c,className:(0,s.A)("tabs__item",x.tabItem,c?.className,{"tabs__item--active":t===n}),children:r??n},n)}))})}function y(e){let{lazy:n,children:r,selectedValue:s}=e;const c=(Array.isArray(r)?r:[r]).filter(Boolean);if(n){const e=c.find((e=>e.props.value===s));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:c.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function b(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,s.A)("tabs-container",x.tabList),children:[(0,j.jsx)(f,{...e,...n}),(0,j.jsx)(y,{...e,...n})]})}function w(e){const n=(0,v.A)();return(0,j.jsx)(b,{...e,children:u(e.children)},String(n))}},28453:(e,n,r)=>{r.d(n,{R:()=>i,x:()=>a});var t=r(96540);const s={},c=t.createContext(s);function i(e){const n=t.useContext(c);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),t.createElement(c.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/73bfd16c.ea4006f0.js b/assets/js/73bfd16c.5d95672c.js similarity index 86% rename from assets/js/73bfd16c.ea4006f0.js rename to assets/js/73bfd16c.5d95672c.js index 94605ae0cc..bd7b6a7050 100644 --- a/assets/js/73bfd16c.ea4006f0.js +++ b/assets/js/73bfd16c.5d95672c.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9525],{33935:e=>{e.exports=JSON.parse('{"label":"release","permalink":"/blog/tags/release","allTagsPath":"/blog/tags","count":24,"unlisted":false}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9525],{33935:e=>{e.exports=JSON.parse('{"label":"release","permalink":"/blog/tags/release","allTagsPath":"/blog/tags","count":25,"unlisted":false}')}}]); \ No newline at end of file diff --git a/assets/js/75ab1baa.32e5afa8.js b/assets/js/75ab1baa.6831b241.js similarity index 60% rename from assets/js/75ab1baa.32e5afa8.js rename to assets/js/75ab1baa.6831b241.js index 9d31efcf8a..2dba594278 100644 --- a/assets/js/75ab1baa.32e5afa8.js +++ b/assets/js/75ab1baa.6831b241.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5885],{84985:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>c,toc:()=>d});var s=t(74848),o=t(28453);const i={title:"Deployment Checklist",sidebar_label:"Checklist"},r=void 0,c={id:"deployment-and-environments/checklist",title:"Deployment Checklist",description:"Set the Node.JS environment to production",source:"@site/docs/deployment-and-environments/checklist.md",sourceDirName:"deployment-and-environments",slug:"/deployment-and-environments/checklist",permalink:"/docs/deployment-and-environments/checklist",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/deployment-and-environments/checklist.md",tags:[],version:"current",frontMatter:{title:"Deployment Checklist",sidebar_label:"Checklist"},sidebar:"someSidebar",previous:{title:"Body Size Limiting",permalink:"/docs/security/body-size-limiting"},next:{title:"Express / Fastify",permalink:"/docs/comparison-with-other-frameworks/express-fastify"}},l={},d=[{value:"Set the Node.JS environment to production",id:"set-the-nodejs-environment-to-production",level:2},{value:"Use HTTPS",id:"use-https",level:2},{value:"Generate Different Secrets",id:"generate-different-secrets",level:2},{value:"Database Credentials & Migrations",id:"database-credentials--migrations",level:2},{value:"Files to Upload",id:"files-to-upload",level:2}];function a(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.h2,{id:"set-the-nodejs-environment-to-production",children:["Set the Node.JS environment to ",(0,s.jsx)(n.code,{children:"production"})]}),"\n",(0,s.jsxs)(n.p,{children:["Set the ",(0,s.jsx)(n.code,{children:"NODE_ENV"})," (or ",(0,s.jsx)(n.code,{children:"FOAL_ENV"}),") environment variable to ",(0,s.jsx)(n.code,{children:"production"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"NODE_ENV=production npm run start\n"})}),"\n",(0,s.jsx)(n.h2,{id:"use-https",children:"Use HTTPS"}),"\n",(0,s.jsxs)(n.p,{children:["You must use HTTPS to prevent ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Man-in-the-middle_attack",children:"man-in-the-middle attacks"}),". Otherwise, your credentials and authentication tokens will appear in clear on the network."]}),"\n",(0,s.jsxs)(n.p,{children:["If you use cookies, make sure to let them only be sent to the server when the request is made using SSL. You can do such thing with the cookie ",(0,s.jsx)(n.code,{children:"secure"})," directive."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"config/production.yml (example)"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",children:"settings:\n # If you use sessions\n session:\n cookie:\n secure: true\n # If you use JWT\n jwt:\n cookie:\n secure: true\n # If you use social authentication\n social:\n cookie:\n secure: true\n"})}),"\n",(0,s.jsx)(n.h2,{id:"generate-different-secrets",children:"Generate Different Secrets"}),"\n",(0,s.jsxs)(n.p,{children:["Use different secrets for your production environment (JWT, etc). Specify them using environment variables or a ",(0,s.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:".env (example)"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"SETTINGS_JWT_SECRET=YZP0iv6gM+VBTxk61l8nKUno2QxsQHO9hm8XfeedZUw\n"})}),"\n",(0,s.jsx)(n.p,{children:"You can generate 256-bit secrets encoded in base64 with the following command:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"foal createsecret\n"})}),"\n",(0,s.jsx)(n.h2,{id:"database-credentials--migrations",children:"Database Credentials & Migrations"}),"\n",(0,s.jsxs)(n.p,{children:["Use different credentials for your production database. Specify them using environment variables or a ",(0,s.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,s.jsx)(n.p,{children:"If you use database migrations, run them on your production server with the following command:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm run migrations\n"})}),"\n",(0,s.jsx)(n.h2,{id:"files-to-upload",children:"Files to Upload"}),"\n",(0,s.jsx)(n.p,{children:"If you install dependencies and build the app on the remote host, then you should upload these files:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-sh",children:"config/\npackage-lock.json\npackage.json\npublic/ # this may depend on how the platform manages static files\nsrc/\ntsconfig.app.json\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Then you will need to run ",(0,s.jsx)(n.code,{children:"npm install"})," and ",(0,s.jsx)(n.code,{children:"npm run build"}),"."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["If you get an error such as ",(0,s.jsx)(n.code,{children:"Foal not found error"}),", it is probably because the dev dependencies (which include the ",(0,s.jsx)(n.code,{children:"@foal/cli"})," package) have not been installed. To force the installation of these dependencies, you can use the following command: ",(0,s.jsx)(n.code,{children:"npm install --production=false"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you install dependencies and build the app on your local host directly, then you should upload these files:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-sh",children:"build/\nconfig/\nnode_modules/\npackage-lock.json\npackage.json\npublic/ # this may depend on how the platform manages static files\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>c});var s=t(96540);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5885],{84985:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>c,toc:()=>d});var s=t(74848),o=t(28453);const i={title:"Deployment Checklist",sidebar_label:"Checklist"},r=void 0,c={id:"deployment-and-environments/checklist",title:"Deployment Checklist",description:"Set the Node.JS environment to production",source:"@site/docs/deployment-and-environments/checklist.md",sourceDirName:"deployment-and-environments",slug:"/deployment-and-environments/checklist",permalink:"/docs/deployment-and-environments/checklist",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/deployment-and-environments/checklist.md",tags:[],version:"current",frontMatter:{title:"Deployment Checklist",sidebar_label:"Checklist"},sidebar:"someSidebar",previous:{title:"Body Size Limiting",permalink:"/docs/security/body-size-limiting"},next:{title:"Express / Fastify",permalink:"/docs/comparison-with-other-frameworks/express-fastify"}},l={},d=[{value:"Set the Node.JS environment to production",id:"set-the-nodejs-environment-to-production",level:2},{value:"Use HTTPS",id:"use-https",level:2},{value:"Generate Different Secrets",id:"generate-different-secrets",level:2},{value:"Database Credentials & Migrations",id:"database-credentials--migrations",level:2},{value:"Files to Upload",id:"files-to-upload",level:2}];function a(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.h2,{id:"set-the-nodejs-environment-to-production",children:["Set the Node.JS environment to ",(0,s.jsx)(n.code,{children:"production"})]}),"\n",(0,s.jsxs)(n.p,{children:["Set the ",(0,s.jsx)(n.code,{children:"NODE_ENV"})," (or ",(0,s.jsx)(n.code,{children:"FOAL_ENV"}),") environment variable to ",(0,s.jsx)(n.code,{children:"production"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"NODE_ENV=production npm run start\n"})}),"\n",(0,s.jsx)(n.h2,{id:"use-https",children:"Use HTTPS"}),"\n",(0,s.jsxs)(n.p,{children:["You must use HTTPS to prevent ",(0,s.jsx)(n.a,{href:"https://en.wikipedia.org/wiki/Man-in-the-middle_attack",children:"man-in-the-middle attacks"}),". Otherwise, your credentials and authentication tokens will appear in clear on the network."]}),"\n",(0,s.jsxs)(n.p,{children:["If you use cookies, make sure to let them only be sent to the server when the request is made using SSL. You can do such thing with the cookie ",(0,s.jsx)(n.code,{children:"secure"})," directive."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"config/production.yml (example)"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-yaml",children:"settings:\n # If you use sessions\n session:\n cookie:\n secure: true\n # If you use JWT\n jwt:\n cookie:\n secure: true\n # If you use social authentication\n social:\n cookie:\n secure: true\n"})}),"\n",(0,s.jsx)(n.h2,{id:"generate-different-secrets",children:"Generate Different Secrets"}),"\n",(0,s.jsxs)(n.p,{children:["Use different secrets for your production environment (JWT, etc). Specify them using environment variables or a ",(0,s.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:".env (example)"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"SETTINGS_JWT_SECRET=YZP0iv6gM+VBTxk61l8nKUno2QxsQHO9hm8XfeedZUw\n"})}),"\n",(0,s.jsx)(n.p,{children:"You can generate 256-bit secrets encoded in base64 with the following command:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npx foal createsecret\n"})}),"\n",(0,s.jsx)(n.h2,{id:"database-credentials--migrations",children:"Database Credentials & Migrations"}),"\n",(0,s.jsxs)(n.p,{children:["Use different credentials for your production database. Specify them using environment variables or a ",(0,s.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,s.jsx)(n.p,{children:"If you use database migrations, run them on your production server with the following command:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm run migrations\n"})}),"\n",(0,s.jsx)(n.h2,{id:"files-to-upload",children:"Files to Upload"}),"\n",(0,s.jsx)(n.p,{children:"If you install dependencies and build the app on the remote host, then you should upload these files:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-sh",children:"config/\npackage-lock.json\npackage.json\npublic/ # this may depend on how the platform manages static files\nsrc/\ntsconfig.app.json\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Then you will need to run ",(0,s.jsx)(n.code,{children:"npm install"})," and ",(0,s.jsx)(n.code,{children:"npm run build"}),"."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["If you get an error such as ",(0,s.jsx)(n.code,{children:"Foal not found error"}),", it is probably because the dev dependencies (which include the ",(0,s.jsx)(n.code,{children:"@foal/cli"})," package) have not been installed. To force the installation of these dependencies, you can use the following command: ",(0,s.jsx)(n.code,{children:"npm install --production=false"}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If you install dependencies and build the app on your local host directly, then you should upload these files:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-sh",children:"build/\nconfig/\nnode_modules/\npackage-lock.json\npackage.json\npublic/ # this may depend on how the platform manages static files\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>c});var s=t(96540);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7a2f366e.3b6152d8.js b/assets/js/7a2f366e.3b6152d8.js deleted file mode 100644 index ac4db473c5..0000000000 --- a/assets/js/7a2f366e.3b6152d8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9639],{10212:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>u,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var s=t(74848),i=t(28453);const r={title:"Models & Queries"},a=void 0,o={id:"databases/typeorm/create-models-and-queries",title:"Models & Queries",description:"Entities",source:"@site/docs/databases/typeorm/create-models-and-queries.md",sourceDirName:"databases/typeorm",slug:"/databases/typeorm/create-models-and-queries",permalink:"/docs/databases/typeorm/create-models-and-queries",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/databases/typeorm/create-models-and-queries.md",tags:[],version:"current",frontMatter:{title:"Models & Queries"},sidebar:"someSidebar",previous:{title:"Introduction",permalink:"/docs/databases/typeorm/introduction"},next:{title:"Migrations",permalink:"/docs/databases/typeorm/generate-and-run-migrations"}},d={},c=[{value:"Entities",id:"entities",level:2},{value:"Using Entities",id:"using-entities",level:3},{value:"Queries",id:"queries",level:2}];function l(e){const n={blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"foal generate entity my-entity\n"})}),"\n",(0,s.jsx)(n.h2,{id:"entities",children:"Entities"}),"\n",(0,s.jsxs)(n.p,{children:["Simple models are called ",(0,s.jsx)(n.em,{children:"entities"})," in TypeORM. You can generate one with the above command."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Product extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n name: string;\n\n @Column()\n price: number;\n\n}\n\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The class ",(0,s.jsx)(n.code,{children:"Product"})," represents the database table and its instances represent the table rows."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["In FoalTS, entity files should always be named with the extension ",(0,s.jsx)(n.code,{children:".entity.ts"}),". This way the CLI can find the entities when automatically generating migrations."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"using-entities",children:"Using Entities"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"const product = new Product();\nproduct.name = 'chair';\nproduct.price = 60;\nawait product.save();\n\nconst products = await Product.find();\n// find by id:\nconst firstProduct = await Product.findOneBy({ id: 1 });\nconst chair = await Product.findOneBy({ name: 'chair' });\n\nawait chair.remove();\n"})}),"\n",(0,s.jsx)(n.h2,{id:"queries",children:"Queries"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"const firstProduct = await Product\n .createQueryBuilder('product')\n .where('product.id = :id', { id: 1 })\n .getOne();\n"})})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var s=t(96540);const i={},r=s.createContext(i);function a(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7a2f366e.f3bab031.js b/assets/js/7a2f366e.f3bab031.js new file mode 100644 index 0000000000..a2edcfde18 --- /dev/null +++ b/assets/js/7a2f366e.f3bab031.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9639],{10212:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>u,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var s=t(74848),i=t(28453);const r={title:"Models & Queries"},a=void 0,o={id:"databases/typeorm/create-models-and-queries",title:"Models & Queries",description:"Entities",source:"@site/docs/databases/typeorm/create-models-and-queries.md",sourceDirName:"databases/typeorm",slug:"/databases/typeorm/create-models-and-queries",permalink:"/docs/databases/typeorm/create-models-and-queries",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/databases/typeorm/create-models-and-queries.md",tags:[],version:"current",frontMatter:{title:"Models & Queries"},sidebar:"someSidebar",previous:{title:"Introduction",permalink:"/docs/databases/typeorm/introduction"},next:{title:"Migrations",permalink:"/docs/databases/typeorm/generate-and-run-migrations"}},d={},c=[{value:"Entities",id:"entities",level:2},{value:"Using Entities",id:"using-entities",level:3},{value:"Queries",id:"queries",level:2}];function l(e){const n={blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",p:"p",pre:"pre",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"npx foal generate entity my-entity\n"})}),"\n",(0,s.jsx)(n.h2,{id:"entities",children:"Entities"}),"\n",(0,s.jsxs)(n.p,{children:["Simple models are called ",(0,s.jsx)(n.em,{children:"entities"})," in TypeORM. You can generate one with the above command."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Example:"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Product extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n name: string;\n\n @Column()\n price: number;\n\n}\n\n"})}),"\n",(0,s.jsxs)(n.p,{children:["The class ",(0,s.jsx)(n.code,{children:"Product"})," represents the database table and its instances represent the table rows."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["In FoalTS, entity files should always be named with the extension ",(0,s.jsx)(n.code,{children:".entity.ts"}),". This way the CLI can find the entities when automatically generating migrations."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"using-entities",children:"Using Entities"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"const product = new Product();\nproduct.name = 'chair';\nproduct.price = 60;\nawait product.save();\n\nconst products = await Product.find();\n// find by id:\nconst firstProduct = await Product.findOneBy({ id: 1 });\nconst chair = await Product.findOneBy({ name: 'chair' });\n\nawait chair.remove();\n"})}),"\n",(0,s.jsx)(n.h2,{id:"queries",children:"Queries"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"const firstProduct = await Product\n .createQueryBuilder('product')\n .where('product.id = :id', { id: 1 })\n .getOne();\n"})})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var s=t(96540);const i={},r=s.createContext(i);function a(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/814f3328.238e47ce.js b/assets/js/814f3328.238e47ce.js new file mode 100644 index 0000000000..c8beeabdcd --- /dev/null +++ b/assets/js/814f3328.238e47ce.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7472],{55513:e=>{e.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Version 4.5 release notes","permalink":"/blog/2024/08/22/version-4.5-release-notes","unlisted":false},{"title":"Version 4.4 release notes","permalink":"/blog/2024/04/25/version-4.4-release-notes","unlisted":false},{"title":"Version 4.3 release notes","permalink":"/blog/2024/04/16/version-4.3-release-notes","unlisted":false},{"title":"Version 4.2 release notes","permalink":"/blog/2023/10/29/version-4.2-release-notes","unlisted":false},{"title":"Version 4.1 release notes","permalink":"/blog/2023/10/24/version-4.1-release-notes","unlisted":false}]}')}}]); \ No newline at end of file diff --git a/assets/js/814f3328.37cadda2.js b/assets/js/814f3328.37cadda2.js deleted file mode 100644 index e5391dd34f..0000000000 --- a/assets/js/814f3328.37cadda2.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7472],{55513:e=>{e.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Version 4.4 release notes","permalink":"/blog/2024/04/25/version-4.4-release-notes","unlisted":false},{"title":"Version 4.3 release notes","permalink":"/blog/2024/04/16/version-4.3-release-notes","unlisted":false},{"title":"Version 4.2 release notes","permalink":"/blog/2023/10/29/version-4.2-release-notes","unlisted":false},{"title":"Version 4.1 release notes","permalink":"/blog/2023/10/24/version-4.1-release-notes","unlisted":false},{"title":"Version 4.0 release notes","permalink":"/blog/2023/09/11/version-4.0-release-notes","unlisted":false}]}')}}]); \ No newline at end of file diff --git a/assets/js/83d480e9.4a0d6b51.js b/assets/js/83d480e9.643cf56f.js similarity index 86% rename from assets/js/83d480e9.4a0d6b51.js rename to assets/js/83d480e9.643cf56f.js index a9d121db89..e6c602d3f7 100644 --- a/assets/js/83d480e9.4a0d6b51.js +++ b/assets/js/83d480e9.643cf56f.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9650],{44078:e=>{e.exports=JSON.parse('{"label":"release","permalink":"/blog/tags/release","allTagsPath":"/blog/tags","count":24,"unlisted":false}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9650],{44078:e=>{e.exports=JSON.parse('{"label":"release","permalink":"/blog/tags/release","allTagsPath":"/blog/tags","count":25,"unlisted":false}')}}]); \ No newline at end of file diff --git a/assets/js/8eb4e46b.31752bf5.js b/assets/js/8eb4e46b.acc4f754.js similarity index 78% rename from assets/js/8eb4e46b.31752bf5.js rename to assets/js/8eb4e46b.acc4f754.js index 791c9c7917..fb3cf29a1b 100644 --- a/assets/js/8eb4e46b.31752bf5.js +++ b/assets/js/8eb4e46b.acc4f754.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5767],{20541:e=>{e.exports=JSON.parse('{"permalink":"/blog/page/2","page":2,"postsPerPage":10,"totalPages":3,"totalCount":25,"previousPage":"/blog","nextPage":"/blog/page/3","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5767],{20541:e=>{e.exports=JSON.parse('{"permalink":"/blog/page/2","page":2,"postsPerPage":10,"totalPages":3,"totalCount":26,"previousPage":"/blog","nextPage":"/blog/page/3","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/assets/js/8f1b2eb6.b54cc022.js b/assets/js/8f1b2eb6.b54cc022.js deleted file mode 100644 index 880f7d53ab..0000000000 --- a/assets/js/8f1b2eb6.b54cc022.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[659],{65034:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>l,toc:()=>a});var o=i(74848),t=i(28453);const s={title:"Configuration"},r=void 0,l={id:"architecture/configuration",title:"Configuration",description:"In FoalTS, configuration refers to any parameter that may vary between deploy environments (production, development, test, etc). It includes sensitive information, such as your database credentials, or simple settings, such as the server port.",source:"@site/docs/architecture/configuration.md",sourceDirName:"architecture",slug:"/architecture/configuration",permalink:"/docs/architecture/configuration",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/configuration.md",tags:[],version:"current",frontMatter:{title:"Configuration"},sidebar:"someSidebar",previous:{title:"Error Handling",permalink:"/docs/architecture/error-handling"},next:{title:"Validation",permalink:"/docs/common/validation-and-sanitization"}},c={},a=[{value:"Configuration Files",id:"configuration-files",level:2},{value:"Deployment Environments",id:"deployment-environments",level:3},{value:"Reserved Parameters",id:"reserved-parameters",level:3},{value:"Accessing Configuration Values",id:"accessing-configuration-values",level:2},{value:"The Config.get method",id:"the-configget-method",level:3},{value:"Specifying a type",id:"specifying-a-type",level:4},{value:"Specifying a default value",id:"specifying-a-default-value",level:4},{value:"The Config.getOrThrow method",id:"the-configgetorthrow-method",level:3},{value:"Environment Variables and .env Files",id:"environment-variables-and-env-files",level:2},{value:"Deployment Environments",id:"deployment-environments-1",level:3},{value:"Using *.local files",id:"using-local-files",level:3},{value:"Note on the use of dotenv",id:"note-on-the-use-of-dotenv",level:3}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(n.p,{children:["In FoalTS, ",(0,o.jsx)(n.em,{children:"configuration"})," refers to any parameter that may vary between deploy environments (production, development, test, etc). It includes sensitive information, such as your database credentials, or simple settings, such as the server port."]}),"\n",(0,o.jsxs)(n.p,{children:["The framework encourages a ",(0,o.jsx)(n.strong,{children:"strict separation between configuration and code"})," and allows you to define your configuration in environment variables, in ",(0,o.jsx)(n.code,{children:".env"})," files and in files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"Config directory structure"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"|- config/\n| |- e2e.json\n| |- default.json\n| |- development.json\n| |- production.json\n| |- ...\n| '- test.json\n|- src/\n'- .env\n"})}),"\n",(0,o.jsx)(n.h2,{id:"configuration-files",children:"Configuration Files"}),"\n",(0,o.jsxs)(n.p,{children:["Configuration values are provided using configuration files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory. Several formats are supported: YAML, JSON and JS files."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.yml"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-yaml",children:'settings:\n session:\n store: "@foal/typeorm"\n'})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.json"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/typeorm"\n }\n }\n}\n'})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.js"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/typeorm"\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.strong,{children:"YAML support"})}),"\n",(0,o.jsxs)(n.p,{children:["The use of YAML for configuration requires the installation of the package ",(0,o.jsx)(n.code,{children:"yamljs"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"npm install yamljs\n"})}),"\n",(0,o.jsxs)(n.p,{children:["When creating a new project, you can also add the flag ",(0,o.jsx)(n.code,{children:"--yaml"})," to have all the configuration directly generated in YAML."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"foal createapp my-app --yaml\n"})}),"\n",(0,o.jsxs)(n.p,{children:["The extension of the YAML files must be ",(0,o.jsx)(n.code,{children:".yml"}),"."]}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"deployment-environments",children:"Deployment Environments"}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.em,{children:"default"})," configuration files are used regardless of the environment, i.e. regardless of the value assigned to the ",(0,o.jsx)(n.code,{children:"NODE_ENV"})," environment variable."]}),"\n",(0,o.jsxs)(n.p,{children:["Configuration values can also be set or overridden for a specific environment using the filename syntax: ",(0,o.jsx)(n.code,{children:"config/.{json|yml|js}"}),". If no value is assigned to ",(0,o.jsx)(n.code,{children:"NODE_ENV"}),", the environment considered is ",(0,o.jsx)(n.em,{children:"development"}),"."]}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsxs)(n.p,{children:["The environment name can be provided in two ways in Foal: via the ",(0,o.jsx)(n.code,{children:"NODE_ENV"})," environment variable or via ",(0,o.jsx)(n.code,{children:"FOAL_ENV"}),". If both of these variables are set, then the value of ",(0,o.jsx)(n.code,{children:"FOAL_ENV"})," is used by the configuration system."]}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"reserved-parameters",children:"Reserved Parameters"}),"\n",(0,o.jsxs)(n.p,{children:["All parameters under the keyword ",(0,o.jsx)(n.code,{children:"settings"})," are reserved for the operation of the framework. You can assign values to those given in the documentation, but you cannot create new ones."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/typeorm"\n }\n },\n "customConfiguration": {\n "message": "hello world"\n }\n}\n'})}),"\n",(0,o.jsx)(n.h2,{id:"accessing-configuration-values",children:"Accessing Configuration Values"}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"Config"})," class provides two static methods ",(0,o.jsx)(n.code,{children:"get"})," and ",(0,o.jsx)(n.code,{children:"getOrThrow"})," for reading configuration values."]}),"\n",(0,o.jsxs)(n.h3,{id:"the-configget-method",children:["The ",(0,o.jsx)(n.code,{children:"Config.get"})," method"]}),"\n",(0,o.jsx)(n.p,{children:"This function takes the configuration key as first parameter."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Config } from '@foal/core';\n\nconst secret = Config.get('settings.jwt.secret');\n"})}),"\n",(0,o.jsx)(n.p,{children:"The algorithm below is used to retrieve the configuration value:"}),"\n",(0,o.jsxs)(n.ol,{children:["\n",(0,o.jsx)(n.li,{children:"Return the value specified in the environment config file if it exists."}),"\n",(0,o.jsxs)(n.li,{children:["Return the value specified in the ",(0,o.jsx)(n.em,{children:"default"})," config file it is exists."]}),"\n",(0,o.jsxs)(n.li,{children:["Return ",(0,o.jsx)(n.code,{children:"undefined"})," otherwise."]}),"\n"]}),"\n",(0,o.jsx)(n.h4,{id:"specifying-a-type",children:"Specifying a type"}),"\n",(0,o.jsx)(n.p,{children:"The method also accepts a second optional parameter to define the type of the returned value."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Config } from '@foal/core';\n\nconst foobar = Config.get('settings.foobar', 'boolean|string');\n// foobar is of type boolean|string|undefined\n"})}),"\n",(0,o.jsxs)(n.p,{children:["When it is set, Foal checks that the configuration value has the correct type and if it does not, it will try to convert it to the desired type (e.g. ",(0,o.jsx)(n.code,{children:'"true"'})," becomes ",(0,o.jsx)(n.code,{children:"true"}),"). If it does not succeed, a ",(0,o.jsx)(n.code,{children:"ConfigTypeError"})," is thrown."]}),"\n",(0,o.jsxs)(n.table,{children:[(0,o.jsx)(n.thead,{children:(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.th,{children:"Allowed types"})})}),(0,o.jsxs)(n.tbody,{children:[(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"string"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"number"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"boolean"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"boolean|string"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"number|string"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"any"})})]})]}),"\n",(0,o.jsx)(n.h4,{id:"specifying-a-default-value",children:"Specifying a default value"}),"\n",(0,o.jsx)(n.p,{children:"The third optional parameter of the method allows you to define a default value if none is found in the configuration."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"const foobar = Config.get('settings.foobar', 'boolean', false);\n// foobar is of type boolean\n"})}),"\n",(0,o.jsxs)(n.h3,{id:"the-configgetorthrow-method",children:["The ",(0,o.jsx)(n.code,{children:"Config.getOrThrow"})," method"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"const foobar = Config.getOrThrow('settings.foobar', 'boolean');\n// foobar is of type boolean\n"})}),"\n",(0,o.jsxs)(n.p,{children:["This method has the same behavior as ",(0,o.jsx)(n.code,{children:"Config.get"})," except that it does not accept a default value. If no value is found, the method will throw a ",(0,o.jsx)(n.code,{children:"ConfigNotFoundError"}),"."]}),"\n",(0,o.jsxs)(n.h2,{id:"environment-variables-and-env-files",children:["Environment Variables and ",(0,o.jsx)(n.code,{children:".env"})," Files"]}),"\n",(0,o.jsxs)(n.p,{children:["Configuration files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory are usually committed and therefore should not contain sensitive information (such as database credentials)."]}),"\n",(0,o.jsxs)(n.p,{children:["The recommended approach to provide sensitive information to the application is to use environment variables and ",(0,o.jsx)(n.code,{children:".env"})," files which are not committed. Then, in the configuration files, the values are retrieved."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:".env"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:'JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n'})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "env(JWT_SECRET)",\n "secretEncoding": "base64"\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(n.p,{children:["If the same variable is provided both as environment variable and in the ",(0,o.jsx)(n.code,{children:".env"})," file, then the value of the environment variable is used."]}),"\n",(0,o.jsx)(n.h3,{id:"deployment-environments-1",children:"Deployment Environments"}),"\n",(0,o.jsxs)(n.p,{children:["Just like the configuration files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory, the ",(0,o.jsx)(n.code,{children:".env"})," files can be used for several environments: ",(0,o.jsx)(n.code,{children:".env.production"}),", ",(0,o.jsx)(n.code,{children:".env.test"}),", etc."]}),"\n",(0,o.jsxs)(n.h3,{id:"using-local-files",children:["Using ",(0,o.jsx)(n.code,{children:"*.local"})," files"]}),"\n",(0,o.jsxs)(n.p,{children:["In case you want to have two ",(0,o.jsx)(n.code,{children:".env"})," files, one to define the default env vars needed by the application and another to override these values on your local machine, you can use a ",(0,o.jsx)(n.code,{children:".env.local"})," file."]}),"\n",(0,o.jsxs)(n.p,{children:["If a variable is defined in both files, the value in the ",(0,o.jsx)(n.code,{children:".env.local"})," file will take precedence."]}),"\n",(0,o.jsxs)(n.p,{children:["Similarly, you can define environment-specific local files (",(0,o.jsx)(n.code,{children:".env.development.local"}),", ",(0,o.jsx)(n.code,{children:".env.production.local"}),", etc)."]}),"\n",(0,o.jsx)(n.h3,{id:"note-on-the-use-of-dotenv",children:"Note on the use of dotenv"}),"\n",(0,o.jsxs)(n.p,{children:["Many NodeJS applications use the ",(0,o.jsx)(n.a,{href:"https://www.npmjs.com/package/dotenv",children:"dotenv"})," library to manage the environment configuration. It loads variables from the ",(0,o.jsx)(n.code,{children:".env"})," file if it exists and assigns their values to the ",(0,o.jsx)(n.code,{children:"process.env"})," object."]}),"\n",(0,o.jsxs)(n.p,{children:["When using Foal, it is strongly recommended that you do not use this library as it may break some functionality. For example, you will not be able to use other files such as ",(0,o.jsx)(n.code,{children:".env.production"})," and ",(0,o.jsx)(n.code,{children:".env.local"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["The recommended approach to loading environment variables from ",(0,o.jsx)(n.code,{children:".env"})," files is to use Foal's configuration system using the ",(0,o.jsx)(n.code,{children:"Config"})," or ",(0,o.jsx)(n.code,{children:"Env"})," class."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"Example"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"// dotenv\nconst value = process.env.FOO_BAR;\n\n// Foal\nimport { Env } from '@foal/core';\n\nconst value = Env.get('FOO_BAR');\n"})})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},28453:(e,n,i)=>{i.d(n,{R:()=>r,x:()=>l});var o=i(96540);const t={},s=o.createContext(t);function r(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/8f1b2eb6.c4e96ce7.js b/assets/js/8f1b2eb6.c4e96ce7.js new file mode 100644 index 0000000000..3a93a08bb6 --- /dev/null +++ b/assets/js/8f1b2eb6.c4e96ce7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[659],{65034:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>l,toc:()=>a});var o=i(74848),t=i(28453);const s={title:"Configuration"},r=void 0,l={id:"architecture/configuration",title:"Configuration",description:"In FoalTS, configuration refers to any parameter that may vary between deploy environments (production, development, test, etc). It includes sensitive information, such as your database credentials, or simple settings, such as the server port.",source:"@site/docs/architecture/configuration.md",sourceDirName:"architecture",slug:"/architecture/configuration",permalink:"/docs/architecture/configuration",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/architecture/configuration.md",tags:[],version:"current",frontMatter:{title:"Configuration"},sidebar:"someSidebar",previous:{title:"Error Handling",permalink:"/docs/architecture/error-handling"},next:{title:"Validation",permalink:"/docs/common/validation-and-sanitization"}},c={},a=[{value:"Configuration Files",id:"configuration-files",level:2},{value:"Deployment Environments",id:"deployment-environments",level:3},{value:"Reserved Parameters",id:"reserved-parameters",level:3},{value:"Accessing Configuration Values",id:"accessing-configuration-values",level:2},{value:"The Config.get method",id:"the-configget-method",level:3},{value:"Specifying a type",id:"specifying-a-type",level:4},{value:"Specifying a default value",id:"specifying-a-default-value",level:4},{value:"The Config.getOrThrow method",id:"the-configgetorthrow-method",level:3},{value:"Environment Variables and .env Files",id:"environment-variables-and-env-files",level:2},{value:"Deployment Environments",id:"deployment-environments-1",level:3},{value:"Using *.local files",id:"using-local-files",level:3},{value:"Note on the use of dotenv",id:"note-on-the-use-of-dotenv",level:3}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(n.p,{children:["In FoalTS, ",(0,o.jsx)(n.em,{children:"configuration"})," refers to any parameter that may vary between deploy environments (production, development, test, etc). It includes sensitive information, such as your database credentials, or simple settings, such as the server port."]}),"\n",(0,o.jsxs)(n.p,{children:["The framework encourages a ",(0,o.jsx)(n.strong,{children:"strict separation between configuration and code"})," and allows you to define your configuration in environment variables, in ",(0,o.jsx)(n.code,{children:".env"})," files and in files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"Config directory structure"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"|- config/\n| |- e2e.json\n| |- default.json\n| |- development.json\n| |- production.json\n| |- ...\n| '- test.json\n|- src/\n'- .env\n"})}),"\n",(0,o.jsx)(n.h2,{id:"configuration-files",children:"Configuration Files"}),"\n",(0,o.jsxs)(n.p,{children:["Configuration values are provided using configuration files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory. Several formats are supported: YAML, JSON and JS files."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.yml"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-yaml",children:'settings:\n session:\n store: "@foal/typeorm"\n'})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.json"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/typeorm"\n }\n }\n}\n'})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.js"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/typeorm"\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.strong,{children:"YAML support"})}),"\n",(0,o.jsxs)(n.p,{children:["The use of YAML for configuration requires the installation of the package ",(0,o.jsx)(n.code,{children:"yamljs"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"npm install yamljs\n"})}),"\n",(0,o.jsxs)(n.p,{children:["When creating a new project, you can also add the flag ",(0,o.jsx)(n.code,{children:"--yaml"})," to have all the configuration directly generated in YAML."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"npx @foal/cli createapp my-app --yaml\n"})}),"\n",(0,o.jsxs)(n.p,{children:["The extension of the YAML files must be ",(0,o.jsx)(n.code,{children:".yml"}),"."]}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"deployment-environments",children:"Deployment Environments"}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.em,{children:"default"})," configuration files are used regardless of the environment, i.e. regardless of the value assigned to the ",(0,o.jsx)(n.code,{children:"NODE_ENV"})," environment variable."]}),"\n",(0,o.jsxs)(n.p,{children:["Configuration values can also be set or overridden for a specific environment using the filename syntax: ",(0,o.jsx)(n.code,{children:"config/.{json|yml|js}"}),". If no value is assigned to ",(0,o.jsx)(n.code,{children:"NODE_ENV"}),", the environment considered is ",(0,o.jsx)(n.em,{children:"development"}),"."]}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsxs)(n.p,{children:["The environment name can be provided in two ways in Foal: via the ",(0,o.jsx)(n.code,{children:"NODE_ENV"})," environment variable or via ",(0,o.jsx)(n.code,{children:"FOAL_ENV"}),". If both of these variables are set, then the value of ",(0,o.jsx)(n.code,{children:"FOAL_ENV"})," is used by the configuration system."]}),"\n"]}),"\n",(0,o.jsx)(n.h3,{id:"reserved-parameters",children:"Reserved Parameters"}),"\n",(0,o.jsxs)(n.p,{children:["All parameters under the keyword ",(0,o.jsx)(n.code,{children:"settings"})," are reserved for the operation of the framework. You can assign values to those given in the documentation, but you cannot create new ones."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/typeorm"\n }\n },\n "customConfiguration": {\n "message": "hello world"\n }\n}\n'})}),"\n",(0,o.jsx)(n.h2,{id:"accessing-configuration-values",children:"Accessing Configuration Values"}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"Config"})," class provides two static methods ",(0,o.jsx)(n.code,{children:"get"})," and ",(0,o.jsx)(n.code,{children:"getOrThrow"})," for reading configuration values."]}),"\n",(0,o.jsxs)(n.h3,{id:"the-configget-method",children:["The ",(0,o.jsx)(n.code,{children:"Config.get"})," method"]}),"\n",(0,o.jsx)(n.p,{children:"This function takes the configuration key as first parameter."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Config } from '@foal/core';\n\nconst secret = Config.get('settings.jwt.secret');\n"})}),"\n",(0,o.jsx)(n.p,{children:"The algorithm below is used to retrieve the configuration value:"}),"\n",(0,o.jsxs)(n.ol,{children:["\n",(0,o.jsx)(n.li,{children:"Return the value specified in the environment config file if it exists."}),"\n",(0,o.jsxs)(n.li,{children:["Return the value specified in the ",(0,o.jsx)(n.em,{children:"default"})," config file it is exists."]}),"\n",(0,o.jsxs)(n.li,{children:["Return ",(0,o.jsx)(n.code,{children:"undefined"})," otherwise."]}),"\n"]}),"\n",(0,o.jsx)(n.h4,{id:"specifying-a-type",children:"Specifying a type"}),"\n",(0,o.jsx)(n.p,{children:"The method also accepts a second optional parameter to define the type of the returned value."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Config } from '@foal/core';\n\nconst foobar = Config.get('settings.foobar', 'boolean|string');\n// foobar is of type boolean|string|undefined\n"})}),"\n",(0,o.jsxs)(n.p,{children:["When it is set, Foal checks that the configuration value has the correct type and if it does not, it will try to convert it to the desired type (e.g. ",(0,o.jsx)(n.code,{children:'"true"'})," becomes ",(0,o.jsx)(n.code,{children:"true"}),"). If it does not succeed, a ",(0,o.jsx)(n.code,{children:"ConfigTypeError"})," is thrown."]}),"\n",(0,o.jsxs)(n.table,{children:[(0,o.jsx)(n.thead,{children:(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.th,{children:"Allowed types"})})}),(0,o.jsxs)(n.tbody,{children:[(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"string"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"number"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"boolean"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"boolean|string"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"number|string"})}),(0,o.jsx)(n.tr,{children:(0,o.jsx)(n.td,{children:"any"})})]})]}),"\n",(0,o.jsx)(n.h4,{id:"specifying-a-default-value",children:"Specifying a default value"}),"\n",(0,o.jsx)(n.p,{children:"The third optional parameter of the method allows you to define a default value if none is found in the configuration."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"const foobar = Config.get('settings.foobar', 'boolean', false);\n// foobar is of type boolean\n"})}),"\n",(0,o.jsxs)(n.h3,{id:"the-configgetorthrow-method",children:["The ",(0,o.jsx)(n.code,{children:"Config.getOrThrow"})," method"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"const foobar = Config.getOrThrow('settings.foobar', 'boolean');\n// foobar is of type boolean\n"})}),"\n",(0,o.jsxs)(n.p,{children:["This method has the same behavior as ",(0,o.jsx)(n.code,{children:"Config.get"})," except that it does not accept a default value. If no value is found, the method will throw a ",(0,o.jsx)(n.code,{children:"ConfigNotFoundError"}),"."]}),"\n",(0,o.jsxs)(n.h2,{id:"environment-variables-and-env-files",children:["Environment Variables and ",(0,o.jsx)(n.code,{children:".env"})," Files"]}),"\n",(0,o.jsxs)(n.p,{children:["Configuration files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory are usually committed and therefore should not contain sensitive information (such as database credentials)."]}),"\n",(0,o.jsxs)(n.p,{children:["The recommended approach to provide sensitive information to the application is to use environment variables and ",(0,o.jsx)(n.code,{children:".env"})," files which are not committed. Then, in the configuration files, the values are retrieved."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:".env"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:'JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n'})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "env(JWT_SECRET)",\n "secretEncoding": "base64"\n }\n }\n}\n'})}),"\n",(0,o.jsxs)(n.p,{children:["If the same variable is provided both as environment variable and in the ",(0,o.jsx)(n.code,{children:".env"})," file, then the value of the environment variable is used."]}),"\n",(0,o.jsx)(n.h3,{id:"deployment-environments-1",children:"Deployment Environments"}),"\n",(0,o.jsxs)(n.p,{children:["Just like the configuration files in the ",(0,o.jsx)(n.code,{children:"config/"})," directory, the ",(0,o.jsx)(n.code,{children:".env"})," files can be used for several environments: ",(0,o.jsx)(n.code,{children:".env.production"}),", ",(0,o.jsx)(n.code,{children:".env.test"}),", etc."]}),"\n",(0,o.jsxs)(n.h3,{id:"using-local-files",children:["Using ",(0,o.jsx)(n.code,{children:"*.local"})," files"]}),"\n",(0,o.jsxs)(n.p,{children:["In case you want to have two ",(0,o.jsx)(n.code,{children:".env"})," files, one to define the default env vars needed by the application and another to override these values on your local machine, you can use a ",(0,o.jsx)(n.code,{children:".env.local"})," file."]}),"\n",(0,o.jsxs)(n.p,{children:["If a variable is defined in both files, the value in the ",(0,o.jsx)(n.code,{children:".env.local"})," file will take precedence."]}),"\n",(0,o.jsxs)(n.p,{children:["Similarly, you can define environment-specific local files (",(0,o.jsx)(n.code,{children:".env.development.local"}),", ",(0,o.jsx)(n.code,{children:".env.production.local"}),", etc)."]}),"\n",(0,o.jsx)(n.h3,{id:"note-on-the-use-of-dotenv",children:"Note on the use of dotenv"}),"\n",(0,o.jsxs)(n.p,{children:["Many NodeJS applications use the ",(0,o.jsx)(n.a,{href:"https://www.npmjs.com/package/dotenv",children:"dotenv"})," library to manage the environment configuration. It loads variables from the ",(0,o.jsx)(n.code,{children:".env"})," file if it exists and assigns their values to the ",(0,o.jsx)(n.code,{children:"process.env"})," object."]}),"\n",(0,o.jsxs)(n.p,{children:["When using Foal, it is strongly recommended that you do not use this library as it may break some functionality. For example, you will not be able to use other files such as ",(0,o.jsx)(n.code,{children:".env.production"})," and ",(0,o.jsx)(n.code,{children:".env.local"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["The recommended approach to loading environment variables from ",(0,o.jsx)(n.code,{children:".env"})," files is to use Foal's configuration system using the ",(0,o.jsx)(n.code,{children:"Config"})," or ",(0,o.jsx)(n.code,{children:"Env"})," class."]}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"Example"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"// dotenv\nconst value = process.env.FOO_BAR;\n\n// Foal\nimport { Env } from '@foal/core';\n\nconst value = Env.get('FOO_BAR');\n"})})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},28453:(e,n,i)=>{i.d(n,{R:()=>r,x:()=>l});var o=i(96540);const t={},s=o.createContext(t);function r(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/902c734f.4acb6820.js b/assets/js/902c734f.4acb6820.js deleted file mode 100644 index 2606dd5912..0000000000 --- a/assets/js/902c734f.4acb6820.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7725],{30438:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>h});var r=t(74848),s=t(28453),i=t(11470),o=t(19365);const a={title:"Authentication with JWT",sidebar_label:"JSON Web Tokens"},l=void 0,c={id:"authentication/jwt",title:"Authentication with JWT",description:"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.",source:"@site/docs/authentication/jwt.md",sourceDirName:"authentication",slug:"/authentication/jwt",permalink:"/docs/authentication/jwt",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/jwt.md",tags:[],version:"current",frontMatter:{title:"Authentication with JWT",sidebar_label:"JSON Web Tokens"},sidebar:"someSidebar",previous:{title:"Session Tokens",permalink:"/docs/authentication/session-tokens"},next:{title:"Social Auth",permalink:"/docs/authentication/social-auth"}},d={},h=[{value:"Generate & Provide a Secret",id:"generate--provide-a-secret",level:2},{value:"Generate & Send Temporary Tokens",id:"generate--send-temporary-tokens",level:2},{value:"Example of a LoginController",id:"example-of-a-logincontroller",level:3},{value:"Receive & Verify Tokens",id:"receive--verify-tokens",level:2},{value:"Advanced",id:"advanced",level:2},{value:"Blacklist Tokens",id:"blacklist-tokens",level:3},{value:"Refresh the tokens",id:"refresh-the-tokens",level:3},{value:"Make a Database Call to Get More User Properties",id:"make-a-database-call-to-get-more-user-properties",level:3},{value:"Specifying a Different Encoding for Secrets",id:"specifying-a-different-encoding-for-secrets",level:3},{value:"Usage with Cookies",id:"usage-with-cookies",level:3},{value:"Cookie options",id:"cookie-options",level:4},{value:"Use RSA or ECDSA public/private keys",id:"use-rsa-or-ecdsa-publicprivate-keys",level:3},{value:"Provide the Public and Private Keys",id:"provide-the-public-and-private-keys",level:4},{value:"Generate Temporary Tokens",id:"generate-temporary-tokens",level:3},{value:"Receive & Verify Tokens",id:"receive--verify-tokens-1",level:4},{value:"Audience, Issuer and Other Options",id:"audience-issuer-and-other-options",level:3},{value:"Retreive a Dynamic Secret Or Public Key",id:"retreive-a-dynamic-secret-or-public-key",level:3},{value:"Retreive a Public Key from a JWKS endpoint",id:"retreive-a-public-key-from-a-jwks-endpoint",level:4},{value:"Auth0 and AWS Cognito (examples)",id:"auth0-and-aws-cognito-examples",level:3},{value:"Hook Errors",id:"hook-errors",level:2}];function u(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install jsonwebtoken @foal/jwt\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA."})}),"\n",(0,r.jsxs)(n.p,{children:["Source: ",(0,r.jsx)(n.a,{href:"https://jwt.io/introduction/",children:"https://jwt.io/introduction/"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Foal offers a package, named ",(0,r.jsx)(n.code,{children:"@foal/jwt"}),", to manage authentication / authorization with JSON Web Tokens. When the user logs in, a token is generated and sent to the client. Then, each subsequent request must include this JWT, allowing the user to access routes, services, and resources that are permitted with that token."]}),"\n",(0,r.jsx)(n.h2,{id:"generate--provide-a-secret",children:"Generate & Provide a Secret"}),"\n",(0,r.jsxs)(n.p,{children:["In order to use JWTs, you must provide a secret to ",(0,r.jsx)(n.em,{children:"sign"})," your tokens. If you do not already have your own, you can generate one with the ",(0,r.jsx)(n.code,{children:"foal createsecret"})," command."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sh",children:"$ foal createsecret\nAk0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Alternatively you can use a public/private key pair to sign your tokens. In this case, please refer to the ",(0,r.jsx)(n.a,{href:"#Use-RSA-or-ECDSA-public/private-keys",children:"advanced section"})," below."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Once the secret is in hand, there are several ways to provide it to the future hooks:"}),"\n",(0,r.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(o.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'settings:\n jwt:\n secret: "Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n secretEncoding: base64\n'})})}),(0,r.jsx)(o.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=",\n "secretEncoding": "base64"\n }\n }\n}\n'})})}),(0,r.jsx)(o.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n jwt: {\n secret: "Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=",\n secretEncoding: "base64"\n }\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h2,{id:"generate--send-temporary-tokens",children:"Generate & Send Temporary Tokens"}),"\n",(0,r.jsx)(n.p,{children:"JSON Web Tokens are generated from JavaScript objects that usually contain information about the user."}),"\n",(0,r.jsx)(n.p,{children:"The below example shows how to generate a one-hour token using a secret."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nconst token = sign(\n {\n sub: '90485234',\n id: 90485234,\n email: 'mary@foalts.org'\n },\n getSecretOrPrivateKey(),\n { expiresIn: '1h' }\n);\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"getSecretOrPrivateKey"})," function tries to read the configurations ",(0,r.jsx)(n.code,{children:"settings.jwt.secret"})," and ",(0,r.jsx)(n.code,{children:"settings.jwt.privateKey"}),". It throws an error if not value is provided. The function ",(0,r.jsx)(n.code,{children:"getSecretOrPublicKey"})," works similarly."]}),"\n"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The ",(0,r.jsx)(n.code,{children:"subject"})," property (or ",(0,r.jsx)(n.code,{children:"sub"}),") is only required when ",(0,r.jsx)(n.a,{href:"#make-a-database-call-to-get-more-user-properties",children:"making a database call to get more user properties"}),"."]}),"\n",(0,r.jsx)(n.li,{children:"Each token should have an expiration time. Otherwise, the JWT will be valid indefinitely, which will raise security issues."}),"\n"]}),"\n",(0,r.jsxs)(n.h3,{id:"example-of-a-logincontroller",children:["Example of a ",(0,r.jsx)(n.code,{children:"LoginController"})]}),"\n",(0,r.jsx)(n.p,{children:"The below example shows how to implement a login controller with an email and a password."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"login.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import {\n Config, Context, HttpResponseOK, HttpResponseUnauthorized,\n Post, ValidateBody, verifyPassword\n} from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nimport { User } from '../entities';\n\nexport class LoginController {\n\n @Post('/login')\n @ValidateBody({\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n })\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n const token = sign(\n { email: user.email },\n getSecretOrPrivateKey(),\n { expiresIn: '1h' }\n );\n\n return new HttpResponseOK({ token });\n }\n\n}\n\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { hashPassword } from '@foal/core';\nimport { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n\n async setPassword(password: string) {\n this.password = await hashPassword(password);\n }\n\n}\n\n"})}),"\n",(0,r.jsx)(n.h2,{id:"receive--verify-tokens",children:"Receive & Verify Tokens"}),"\n",(0,r.jsxs)(n.p,{children:["Foal provides two hooks to authenticate users upon subsequent requests: ",(0,r.jsx)(n.code,{children:"JWTOptional"})," and ",(0,r.jsx)(n.code,{children:"JWTRequired"}),". They both expect the client to send the JWT in the ",(0,r.jsx)(n.strong,{children:"Authorization"})," header using the ",(0,r.jsx)(n.strong,{children:"Bearer"})," schema."]}),"\n",(0,r.jsx)(n.p,{children:"In other words, the content of the header should look like the following:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Authorization: Bearer \n"})}),"\n",(0,r.jsxs)(n.p,{children:["If no token is provided, the ",(0,r.jsx)(n.code,{children:"JWTRequired"})," hook returns an error ",(0,r.jsx)(n.em,{children:"400 - BAD REQUEST"})," while ",(0,r.jsx)(n.code,{children:"JWTOptional"})," does nothing."]}),"\n",(0,r.jsxs)(n.p,{children:["If a token is provided and valid, the hooks set the ",(0,r.jsx)(n.code,{children:"Context.user"})," with the decoded payload (default behavior)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired()\nexport class ApiController {\n\n @Get('/products')\n readProducts(ctx: Context) {\n console.log(ctx.user);\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,r.jsx)(n.h3,{id:"blacklist-tokens",children:"Blacklist Tokens"}),"\n",(0,r.jsxs)(n.p,{children:["In the event that a jwt has been stolen by an attacker, the application must be able to revoke the compromised token. This can be done by establishing a ",(0,r.jsx)(n.em,{children:"black list"}),". Revoked tokens are no longer considered valid and the hooks return a 401 error - UNAUTHORIZED when they receive one."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { isInFile } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({ blackList: isInFile('./blacklist.txt') })\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"isInFile"})," function takes a token and returns a boolean specifying if the token is revoked or not."]}),"\n",(0,r.jsx)(n.p,{children:"You can provide your own function (in the case you want to use a cache database for example). This function must have this signature:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"(token: string) => boolean|Promise;\n"})}),"\n",(0,r.jsx)(n.h3,{id:"refresh-the-tokens",children:"Refresh the tokens"}),"\n",(0,r.jsx)(n.p,{children:"Having a too-long expiration date for JSON Web Tokens is not recommend as it increases exposure to attacks based on token hijacking. If an attacker succeeds in stealing a token with an insufficient expiration date, he/she will have plenty of time to make other attacks and harm your application."}),"\n",(0,r.jsx)(n.p,{children:"In order to minimize the exposure, it is recommend to set a short expiration date (15 minutes for common applications) to quickly invalidate tokens. In this way, even if a token is stolen, it will quickly become unusable since it will have expired."}),"\n",(0,r.jsx)(n.p,{children:"One of the disadvantages of having short expiration dates, however, is that users get logged out too often which is not very user-friendly."}),"\n",(0,r.jsx)(n.p,{children:"One way to get around this problem is to generate and send a new token on each request. The client then saves this new token and uses it on further requests. In this way, if users are inactive more than 15 minutes, they are disconnected. Otherwise, the user will still be connected but the application will use a different token."}),"\n",(0,r.jsxs)(n.p,{children:["The below code shows how to implement this technique with a hook. On each request, the client will receive a new token in the ",(0,r.jsx)(n.code,{children:"Authorization"})," header of the response. Other implementations are still possible (especially if you use cookies)."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Note that when a new token is generated, the previous one is still valid until its expiration date."})}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"refresh-jwt.hook.ts (example)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Hook, HookDecorator, HttpResponse, isHttpResponseServerError } from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nexport function RefreshJWT(): HookDecorator {\n return Hook(ctx => {\n if (!ctx.user) {\n return;\n }\n\n return (response: HttpResponse) => {\n if (isHttpResponseServerError(response)) {\n return;\n }\n\n const newToken = sign(\n // The below object assumes that ctx.user is\n // the decoded payload (default behavior).\n {\n email: ctx.user.email,\n // id: ctx.user.id,\n // sub: ctx.user.subject,\n },\n getSecretOrPrivateKey(),\n { expiresIn: '15m' }\n );\n response.setHeader('Authorization', newToken);\n };\n\n });\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"api.controller.ts (example)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"@JWTRequired()\n@RefreshJWT()\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"make-a-database-call-to-get-more-user-properties",children:"Make a Database Call to Get More User Properties"}),"\n",(0,r.jsxs)(n.p,{children:["In several cases, the decoded payload is not sufficient. We may need to fetch extra properties from the database, such as the user permissions for example, or simply want the ",(0,r.jsx)(n.code,{children:"Context.user"})," to a be a model instance instead of a plain object."]}),"\n",(0,r.jsxs)(n.p,{children:["In these cases, the two hooks ",(0,r.jsx)(n.code,{children:"JWTRequired"})," and ",(0,r.jsx)(n.code,{children:"JWTOptional"})," offer a ",(0,r.jsx)(n.code,{children:"user"})," option to transform the decoded payload into something else. To do this,"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:["Each JSON Web Token must have a ",(0,r.jsx)(n.code,{children:"subject"})," property (or ",(0,r.jsx)(n.code,{children:"sub"}),") which is a string containing the user id. If the id is a number, it must be converted to a string using, for example, the ",(0,r.jsx)(n.code,{children:"toString()"})," method."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nconst token = sign(\n {\n // TypeScript v3.5.1 and v3.5.2 have a bug which makes the compilation fail\n // with the property \"sub\". This can be fixed by adding \"as any\" after the object.\n sub: '90485234', // Required\n id: 90485234,\n email: 'mary@foalts.org'\n },\n getSecretOrPrivateKey(),\n { expiresIn: '1h' }\n);\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:["The hook must be provided a function that takes a string id (the ",(0,r.jsx)(n.code,{children:"subject"}),") as parameter and returns the value of the ",(0,r.jsx)(n.code,{children:"Context.user"}),". If the function returns ",(0,r.jsx)(n.code,{children:"undefined"}),", the hooks returns an error ",(0,r.jsx)(n.em,{children:"401 - UNAUTHORIZED"}),"."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with TypeORM (SQL database)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nimport { User } from '../entities';\n\n@JWTRequired({ user: (id: number) => User.findOneBy({ id }) })\nexport class ApiController {\n @Get('/do-something')\n get(ctx: Context) {\n // ctx.user is the instance returned by User.findOneBy.\n // ...\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with TypeORM (MongoDB)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\nimport { ObjectId } from 'mongodb';\n\nimport { User } from '../entities';\n\n@JWTRequired({\n userIdType: 'string',\n user: (id: string) => User.findOneBy({ _id: new ObjectId(id) }),\n})\nexport class ApiController {\n @Get('/do-something')\n get(ctx: Context) {\n // ctx.user is the instance returned by User.findOneBy.\n // ...\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with a custom function"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nconst users = [\n { id: 1, email: 'mary@foalts.org', isAdmin: true },\n { id: 2, email: 'john@foalts.org', isAdmin: false },\n];\n\nfunction getUserById(id: number) {\n return users.find(user => user.id === id);\n}\n\n@JWTRequired({ user: getUserById })\nexport class ApiController {\n @Get('/do-something')\n get(ctx: Context) {\n // ctx.user is an item of the `users` array.\n // ...\n }\n}\n"})}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"specifying-a-different-encoding-for-secrets",children:"Specifying a Different Encoding for Secrets"}),"\n",(0,r.jsxs)(n.p,{children:["By default, UTF-8 is used to encode the secret string into bytes when verifying the token. However, you can use another character encoding with the ",(0,r.jsx)(n.code,{children:"settings.jwt.secretEncoding"})," configuration key."]}),"\n",(0,r.jsxs)(n.p,{children:["Available encodings are listed ",(0,r.jsx)(n.a,{href:"https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings",children:"here"}),"."]}),"\n",(0,r.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(o.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n secret: HEwh0TW7w6a5yUwIrpHilUqetAqTFAVSHx2rg6DWNtg=\n secretEncoding: base64\n"})})}),(0,r.jsx)(o.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "HEwh0TW7w6a5yUwIrpHilUqetAqTFAVSHx2rg6DWNtg=",\n "secretEncoding": "base64",\n }\n }\n}\n'})})}),(0,r.jsx)(o.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n jwt: {\n secret: "HEwh0TW7w6a5yUwIrpHilUqetAqTFAVSHx2rg6DWNtg=",\n secretEncoding: "base64",\n }\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h3,{id:"usage-with-cookies",children:"Usage with Cookies"}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Be aware that if you use cookies, your application must provide a ",(0,r.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF defense"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["By default, the hooks expect the token to be sent in the ",(0,r.jsx)(n.strong,{children:"Authorization"})," header using the ",(0,r.jsx)(n.strong,{children:"Bearer"})," schema. But it is also possible to send the token in a cookie with the ",(0,r.jsx)(n.code,{children:"cookie"})," option."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"api.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({ cookie: true })\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"auth.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"\nimport { setAuthCookie, removeAuthCookie } from '@foal/jwt';\n\nexport class AuthController {\n\n @Post('/login')\n async login(ctx: Context) {\n // ...\n\n const response = new HttpResponseNoContent();\n // Do not forget the \"await\" keyword.\n await setAuthCookie(response, token);\n return response;\n }\n\n @Post('/logout')\n logout(ctx: Context) {\n // ...\n\n const response = new HttpResponseNoContent();\n removeAuthCookie(response);\n return response;\n }\n\n}\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Note: the cookie expire date is equal to the JWT expire date."})}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"cookie-options",children:"Cookie options"}),"\n",(0,r.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(o.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n cookie:\n name: mycookiename # Default: auth\n domain: example.com\n httpOnly: true # Warning: unlike session tokens, the httpOnly directive has no default value.\n path: /foo # Default: /\n sameSite: strict # Default: lax if settings.jwt.csrf.enabled is true.\n secure: true\n"})})}),(0,r.jsx)(o.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "cookie": {\n "name": "mycookiename",\n "domain": "example.com",\n "httpOnly": true,\n "path": "/foo",\n "sameSite": "strict",\n "secure": true\n }\n }\n }\n}\n'})})}),(0,r.jsx)(o.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n jwt: {\n cookie: {\n name: "mycookiename",\n domain: "example.com",\n httpOnly: true,\n path: "/foo",\n sameSite: "strict",\n secure: true\n }\n }\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h3,{id:"use-rsa-or-ecdsa-publicprivate-keys",children:"Use RSA or ECDSA public/private keys"}),"\n",(0,r.jsx)(n.p,{children:"JWTs can also be signed using a public/private key pair using RSA or ECDSA."}),"\n",(0,r.jsx)(n.h4,{id:"provide-the-public-and-private-keys",children:"Provide the Public and Private Keys"}),"\n",(0,r.jsx)(n.p,{children:"First of all, specify in the configuration where the keys are stored."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"config/default.js"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:"const { Env } = require('@foal/core');\nconst { readFileSync } = require('fs');\n\nmodule.exports = {\n settings: {\n jwt: {\n privateKey: Env.get('RSA_PRIVATE_KEY') || readFileSync('./id_rsa', 'utf8'),\n publicKey: Env.get('RSA_PUBLIC_KEY') || readFileSync('./id_rsa.pub', 'utf8'),\n }\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Then you can provide the keys in RSA files (",(0,r.jsx)(n.code,{children:"id_rsa"})," and ",(0,r.jsx)(n.code,{children:".id_rsa/pub"}),") or in environment variables."]}),"\n",(0,r.jsx)(n.h3,{id:"generate-temporary-tokens",children:"Generate Temporary Tokens"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Config } from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nconst token = sign(\n {\n email: 'john@foalts.org'\n },\n getSecretOrPrivateKey(),\n { expiresIn: '1h', algorithm: 'RS256' }\n);\n"})}),"\n",(0,r.jsx)(n.h4,{id:"receive--verify-tokens-1",children:"Receive & Verify Tokens"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with RSA"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({}, { algorithm: 'RS256' })\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.h3,{id:"audience-issuer-and-other-options",children:"Audience, Issuer and Other Options"}),"\n",(0,r.jsxs)(n.p,{children:["The second parameter of ",(0,r.jsx)(n.code,{children:"JWTOptional"})," and ",(0,r.jsx)(n.code,{children:"JWTRequired"})," allows to specify the required audience or issuer as well as other properties. It is passed as options to the ",(0,r.jsx)(n.code,{children:"verify"})," function of the ",(0,r.jsx)(n.a,{href:"https://www.npmjs.com/package/jsonwebtoken",children:"jsonwebtoken"})," library."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example checking the audience"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({}, { audience: [ /urn:f[o]{2}/, 'urn:bar' ] })\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example checking the issuer"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({}, { issuer: 'foo' })\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.h3,{id:"retreive-a-dynamic-secret-or-public-key",children:"Retreive a Dynamic Secret Or Public Key"}),"\n",(0,r.jsxs)(n.p,{children:["By default ",(0,r.jsx)(n.code,{children:"JWTRequired"})," and ",(0,r.jsx)(n.code,{children:"JWTOptional"})," use the value of the configuration keys ",(0,r.jsx)(n.code,{children:"settings.jwt.secret"})," or ",(0,r.jsx)(n.code,{children:"settings.jwt.publicKey"})," as a static secret (or public key)."]}),"\n",(0,r.jsxs)(n.p,{children:["But it is also possible to dynamically retrieve a key to verify the token. To do so, you can specify a function with the below signature to the ",(0,r.jsx)(n.code,{children:"secretOrPublicKey"})," option."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"(header: any, payload: any) => Promise;\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n secretOrPublicKey: async (header, payload) => {\n // ...\n return 'my-secret';\n }\n})\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If needed, this function can throw an ",(0,r.jsx)(n.code,{children:"InvalidTokenError"})," to return a 401 error to the client."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n secretOrPublicKey: async (header, payload) => {\n if (header.alg !== 'RS256') {\n throw new InvalidTokenError('invalid algorithm');\n }\n return 'my-secret';\n }\n})\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["In the above example, if the algorithm specified in the token is not ",(0,r.jsx)(n.code,{children:"RS256"}),", then the server will respond a ",(0,r.jsx)(n.code,{children:"401 - UNAUTHORIZED"})," error with this content:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"{\n code: 'invalid_token',\n description: 'invalid algorithm'\n}\n"})}),"\n",(0,r.jsx)(n.h4,{id:"retreive-a-public-key-from-a-jwks-endpoint",children:"Retreive a Public Key from a JWKS endpoint"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install @foal/jwks-rsa\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"getRSAPublicKeyFromJWKS"})," allows you to retreive a public key from a JWKS endpoint. It is based on the ",(0,r.jsxs)(n.a,{href:"https://github.com/auth0/node-jwks-rsa",children:[(0,r.jsx)(n.code,{children:"jwks-rsa"})," library"]}),"."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n secretOrPublicKey: getRSAPublicKeyFromJWKS({\n cache: true,\n cacheMaxEntries: 5, // Default value\n cacheMaxAge: ms('10h'), // Default value\n jwksUri: 'http://localhost:3000/.well-known/jwks.json',\n })\n})\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.h3,{id:"auth0-and-aws-cognito-examples",children:"Auth0 and AWS Cognito (examples)"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install @foal/jwks-rsa\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:"Auth0 & AWS Cognito are both platforms to manage authentication & authorization."}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["This section provides examples on how to decode and verify JWTs generated by these platforms (the ",(0,r.jsx)(n.code,{children:"id_token"}),"). It assumes that you are already familiar with them."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Auth0"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n// These lines assume that you provided your DOMAIN and AUDIENCE in either\n// an .env file, in environment variables or in one the configuration file \n// in `config/`.\nconst domain = Config.getOrThrow('auth0.domain', 'string');\nconst audience = Config.getOrThrow('auth0.audience', 'string');\n\n@JWTRequired({\n secretOrPublicKey: getRSAPublicKeyFromJWKS({\n cache: true,\n jwksRequestsPerMinute: 5,\n jwksUri: `https://${domain}/.well-known/jwks.json`,\n rateLimit: true,\n })\n}, {\n algorithms: [ 'RS256' ],\n audience,\n issuer: `https://${domain}/`,\n})\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"AWS Cognito"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n// These lines assume that you provided your CLIENT_ID, DOMAIN and USER_POOL_ID\n// in either an .env file, in environment variables or in one the configuration \n// file in `config/`.\nconst clientId = Config.getOrThrow('cognito.clientId', 'string');\nconst domain = Config.getOrThrow('cognito.domain', 'string');\nconst userPoolId = Config.getOrThrow('cognito.userPoolId', 'string');\n\n@JWTRequired({\n secretOrPublicKey: getRSAPublicKeyFromJWKS({\n cache: true,\n jwksRequestsPerMinute: 5,\n jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`,\n rateLimit: true,\n })\n}, {\n algorithms: [ 'RS256' ],\n audience: clientId,\n issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,\n})\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Note: The above example does not use a secret for simplicity."})}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"hook-errors",children:"Hook Errors"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Error"}),(0,r.jsx)(n.th,{children:"Response Status"}),(0,r.jsx)(n.th,{children:"Response Body"}),(0,r.jsxs)(n.th,{children:[(0,r.jsx)(n.code,{children:"WWW-Authenticate"})," Response Header"]})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["No secret or public key is provided in ",(0,r.jsx)(n.code,{children:"default.json"})," or as environment variable."]}),(0,r.jsx)(n.td,{children:"500"}),(0,r.jsx)(n.td,{}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["The ",(0,r.jsx)(n.code,{children:"Authorization"})," header does not exist (only for ",(0,r.jsx)(n.code,{children:"JWTRequired"}),")."]}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_request', description: 'Authorization header not found.' }"})}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["The auth cookie does not exist (only for ",(0,r.jsx)(n.code,{children:"JWTRequired"}),")."]}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_request', description: 'Auth cookie not found.' }"})}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["The ",(0,r.jsx)(n.code,{children:"Authorization"})," header does use the Bearer scheme."]}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_request', description: 'Expected a bearer token. Scheme is Authorization: Bearer .' }"})}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The token is black listed."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt revoked' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt revoked"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The token is not a JWT."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt malformed' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt malformed"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The signature is invalid."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt signature' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt signature"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The token is expired."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt expired' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt expired"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The audience is not expected."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt audience invalid. expected: xxx' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt audience invalid. expected: xxx"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The issuer is not expected."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt issuer invalid. expected: xxx' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt issuer invalid. expected: xxx"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["There is no subject claim and ",(0,r.jsx)(n.code,{children:"options.user"})," is defined."]}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'The token must include a subject which is the id of the user.' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="The token must include a subject which is the id of the user."'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"options.user"})," is defined and no user was found from its value (function)."]}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'The token subject does not match any user.' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="The token subject does not match any user."'})]})]})]})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},19365:(e,n,t)=>{t.d(n,{A:()=>o});t(96540);var r=t(34164);const s={tabItem:"tabItem_Ymn6"};var i=t(74848);function o(e){let{children:n,hidden:t,className:o}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,r.A)(s.tabItem,o),hidden:t,children:n})}},11470:(e,n,t)=>{t.d(n,{A:()=>k});var r=t(96540),s=t(34164),i=t(23104),o=t(56347),a=t(205),l=t(57485),c=t(31682),d=t(89466);function h(e){return r.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:t,attributes:r,default:s}}=e;return{value:n,label:t,attributes:r,default:s}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:t}=e;const s=(0,o.W6)(),i=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,l.aZ)(i),(0,r.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(s.location.search);n.set(i,e),s.replace({...s.location,search:n.toString()})}),[i,s])]}function j(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,i=u(e),[o,l]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:i}))),[c,h]=x({queryString:t,groupId:s}),[j,m]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,i]=(0,d.Dv)(t);return[s,(0,r.useCallback)((e=>{t&&i.set(e)}),[t,i])]}({groupId:s}),f=(()=>{const e=c??j;return p({value:e,tabValues:i})?e:null})();(0,a.A)((()=>{f&&l(f)}),[f]);return{selectedValue:o,selectValue:(0,r.useCallback)((e=>{if(!p({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);l(e),h(e),m(e)}),[h,m,i]),tabValues:i}}var m=t(92303);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var g=t(74848);function v(e){let{className:n,block:t,selectedValue:r,selectValue:o,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const n=e.currentTarget,t=l.indexOf(n),s=a[t].value;s!==r&&(c(n),o(s))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=l.indexOf(e.currentTarget)+1;n=l[t]??l[0];break}case"ArrowLeft":{const t=l.indexOf(e.currentTarget)-1;n=l[t]??l[l.length-1];break}}n?.focus()};return(0,g.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":t},n),children:a.map((e=>{let{value:n,label:t,attributes:i}=e;return(0,g.jsx)("li",{role:"tab",tabIndex:r===n?0:-1,"aria-selected":r===n,ref:e=>l.push(e),onKeyDown:h,onClick:d,...i,className:(0,s.A)("tabs__item",f.tabItem,i?.className,{"tabs__item--active":r===n}),children:t??n},n)}))})}function y(e){let{lazy:n,children:t,selectedValue:s}=e;const i=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===s));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,g.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function b(e){const n=j(e);return(0,g.jsxs)("div",{className:(0,s.A)("tabs-container",f.tabList),children:[(0,g.jsx)(v,{...e,...n}),(0,g.jsx)(y,{...e,...n})]})}function k(e){const n=(0,m.A)();return(0,g.jsx)(b,{...e,children:h(e.children)},String(n))}},28453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>a});var r=t(96540);const s={},i=r.createContext(s);function o(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/902c734f.c74df078.js b/assets/js/902c734f.c74df078.js new file mode 100644 index 0000000000..33ed1f9e1e --- /dev/null +++ b/assets/js/902c734f.c74df078.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7725],{30438:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>h});var r=t(74848),s=t(28453),i=t(11470),o=t(19365);const a={title:"Authentication with JWT",sidebar_label:"JSON Web Tokens"},l=void 0,c={id:"authentication/jwt",title:"Authentication with JWT",description:"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.",source:"@site/docs/authentication/jwt.md",sourceDirName:"authentication",slug:"/authentication/jwt",permalink:"/docs/authentication/jwt",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/jwt.md",tags:[],version:"current",frontMatter:{title:"Authentication with JWT",sidebar_label:"JSON Web Tokens"},sidebar:"someSidebar",previous:{title:"Session Tokens",permalink:"/docs/authentication/session-tokens"},next:{title:"Social Auth",permalink:"/docs/authentication/social-auth"}},d={},h=[{value:"Generate & Provide a Secret",id:"generate--provide-a-secret",level:2},{value:"Generate & Send Temporary Tokens",id:"generate--send-temporary-tokens",level:2},{value:"Example of a LoginController",id:"example-of-a-logincontroller",level:3},{value:"Receive & Verify Tokens",id:"receive--verify-tokens",level:2},{value:"Advanced",id:"advanced",level:2},{value:"Blacklist Tokens",id:"blacklist-tokens",level:3},{value:"Refresh the tokens",id:"refresh-the-tokens",level:3},{value:"Make a Database Call to Get More User Properties",id:"make-a-database-call-to-get-more-user-properties",level:3},{value:"Specifying a Different Encoding for Secrets",id:"specifying-a-different-encoding-for-secrets",level:3},{value:"Usage with Cookies",id:"usage-with-cookies",level:3},{value:"Cookie options",id:"cookie-options",level:4},{value:"Use RSA or ECDSA public/private keys",id:"use-rsa-or-ecdsa-publicprivate-keys",level:3},{value:"Provide the Public and Private Keys",id:"provide-the-public-and-private-keys",level:4},{value:"Generate Temporary Tokens",id:"generate-temporary-tokens",level:3},{value:"Receive & Verify Tokens",id:"receive--verify-tokens-1",level:4},{value:"Audience, Issuer and Other Options",id:"audience-issuer-and-other-options",level:3},{value:"Retreive a Dynamic Secret Or Public Key",id:"retreive-a-dynamic-secret-or-public-key",level:3},{value:"Retreive a Public Key from a JWKS endpoint",id:"retreive-a-public-key-from-a-jwks-endpoint",level:4},{value:"Auth0 and AWS Cognito (examples)",id:"auth0-and-aws-cognito-examples",level:3},{value:"Hook Errors",id:"hook-errors",level:2}];function u(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install jsonwebtoken @foal/jwt\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA."})}),"\n",(0,r.jsxs)(n.p,{children:["Source: ",(0,r.jsx)(n.a,{href:"https://jwt.io/introduction/",children:"https://jwt.io/introduction/"})]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Foal offers a package, named ",(0,r.jsx)(n.code,{children:"@foal/jwt"}),", to manage authentication / authorization with JSON Web Tokens. When the user logs in, a token is generated and sent to the client. Then, each subsequent request must include this JWT, allowing the user to access routes, services, and resources that are permitted with that token."]}),"\n",(0,r.jsx)(n.h2,{id:"generate--provide-a-secret",children:"Generate & Provide a Secret"}),"\n",(0,r.jsxs)(n.p,{children:["In order to use JWTs, you must provide a secret to ",(0,r.jsx)(n.em,{children:"sign"})," your tokens. If you do not already have your own, you can generate one with the ",(0,r.jsx)(n.code,{children:"npx foal createsecret"})," command."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-sh",children:"npx foal createsecret\nAk0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Alternatively you can use a public/private key pair to sign your tokens. In this case, please refer to the ",(0,r.jsx)(n.a,{href:"#Use-RSA-or-ECDSA-public/private-keys",children:"advanced section"})," below."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Once the secret is in hand, there are several ways to provide it to the future hooks:"}),"\n",(0,r.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(o.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:'settings:\n jwt:\n secret: "Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n secretEncoding: base64\n'})})}),(0,r.jsx)(o.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=",\n "secretEncoding": "base64"\n }\n }\n}\n'})})}),(0,r.jsx)(o.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n jwt: {\n secret: "Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=",\n secretEncoding: "base64"\n }\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h2,{id:"generate--send-temporary-tokens",children:"Generate & Send Temporary Tokens"}),"\n",(0,r.jsx)(n.p,{children:"JSON Web Tokens are generated from JavaScript objects that usually contain information about the user."}),"\n",(0,r.jsx)(n.p,{children:"The below example shows how to generate a one-hour token using a secret."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nconst token = sign(\n {\n sub: '90485234',\n id: 90485234,\n email: 'mary@foalts.org'\n },\n getSecretOrPrivateKey(),\n { expiresIn: '1h' }\n);\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"getSecretOrPrivateKey"})," function tries to read the configurations ",(0,r.jsx)(n.code,{children:"settings.jwt.secret"})," and ",(0,r.jsx)(n.code,{children:"settings.jwt.privateKey"}),". It throws an error if not value is provided. The function ",(0,r.jsx)(n.code,{children:"getSecretOrPublicKey"})," works similarly."]}),"\n"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The ",(0,r.jsx)(n.code,{children:"subject"})," property (or ",(0,r.jsx)(n.code,{children:"sub"}),") is only required when ",(0,r.jsx)(n.a,{href:"#make-a-database-call-to-get-more-user-properties",children:"making a database call to get more user properties"}),"."]}),"\n",(0,r.jsx)(n.li,{children:"Each token should have an expiration time. Otherwise, the JWT will be valid indefinitely, which will raise security issues."}),"\n"]}),"\n",(0,r.jsxs)(n.h3,{id:"example-of-a-logincontroller",children:["Example of a ",(0,r.jsx)(n.code,{children:"LoginController"})]}),"\n",(0,r.jsx)(n.p,{children:"The below example shows how to implement a login controller with an email and a password."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"login.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import {\n Config, Context, HttpResponseOK, HttpResponseUnauthorized,\n Post, ValidateBody, verifyPassword\n} from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nimport { User } from '../entities';\n\nexport class LoginController {\n\n @Post('/login')\n @ValidateBody({\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n })\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n const token = sign(\n { email: user.email },\n getSecretOrPrivateKey(),\n { expiresIn: '1h' }\n );\n\n return new HttpResponseOK({ token });\n }\n\n}\n\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { hashPassword } from '@foal/core';\nimport { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n\n async setPassword(password: string) {\n this.password = await hashPassword(password);\n }\n\n}\n\n"})}),"\n",(0,r.jsx)(n.h2,{id:"receive--verify-tokens",children:"Receive & Verify Tokens"}),"\n",(0,r.jsxs)(n.p,{children:["Foal provides two hooks to authenticate users upon subsequent requests: ",(0,r.jsx)(n.code,{children:"JWTOptional"})," and ",(0,r.jsx)(n.code,{children:"JWTRequired"}),". They both expect the client to send the JWT in the ",(0,r.jsx)(n.strong,{children:"Authorization"})," header using the ",(0,r.jsx)(n.strong,{children:"Bearer"})," schema."]}),"\n",(0,r.jsx)(n.p,{children:"In other words, the content of the header should look like the following:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Authorization: Bearer \n"})}),"\n",(0,r.jsxs)(n.p,{children:["If no token is provided, the ",(0,r.jsx)(n.code,{children:"JWTRequired"})," hook returns an error ",(0,r.jsx)(n.em,{children:"400 - BAD REQUEST"})," while ",(0,r.jsx)(n.code,{children:"JWTOptional"})," does nothing."]}),"\n",(0,r.jsxs)(n.p,{children:["If a token is provided and valid, the hooks set the ",(0,r.jsx)(n.code,{children:"Context.user"})," with the decoded payload (default behavior)."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired()\nexport class ApiController {\n\n @Get('/products')\n readProducts(ctx: Context) {\n console.log(ctx.user);\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,r.jsx)(n.h3,{id:"blacklist-tokens",children:"Blacklist Tokens"}),"\n",(0,r.jsxs)(n.p,{children:["In the event that a jwt has been stolen by an attacker, the application must be able to revoke the compromised token. This can be done by establishing a ",(0,r.jsx)(n.em,{children:"black list"}),". Revoked tokens are no longer considered valid and the hooks return a 401 error - UNAUTHORIZED when they receive one."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { isInFile } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({ blackList: isInFile('./blacklist.txt') })\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"isInFile"})," function takes a token and returns a boolean specifying if the token is revoked or not."]}),"\n",(0,r.jsx)(n.p,{children:"You can provide your own function (in the case you want to use a cache database for example). This function must have this signature:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"(token: string) => boolean|Promise;\n"})}),"\n",(0,r.jsx)(n.h3,{id:"refresh-the-tokens",children:"Refresh the tokens"}),"\n",(0,r.jsx)(n.p,{children:"Having a too-long expiration date for JSON Web Tokens is not recommend as it increases exposure to attacks based on token hijacking. If an attacker succeeds in stealing a token with an insufficient expiration date, he/she will have plenty of time to make other attacks and harm your application."}),"\n",(0,r.jsx)(n.p,{children:"In order to minimize the exposure, it is recommend to set a short expiration date (15 minutes for common applications) to quickly invalidate tokens. In this way, even if a token is stolen, it will quickly become unusable since it will have expired."}),"\n",(0,r.jsx)(n.p,{children:"One of the disadvantages of having short expiration dates, however, is that users get logged out too often which is not very user-friendly."}),"\n",(0,r.jsx)(n.p,{children:"One way to get around this problem is to generate and send a new token on each request. The client then saves this new token and uses it on further requests. In this way, if users are inactive more than 15 minutes, they are disconnected. Otherwise, the user will still be connected but the application will use a different token."}),"\n",(0,r.jsxs)(n.p,{children:["The below code shows how to implement this technique with a hook. On each request, the client will receive a new token in the ",(0,r.jsx)(n.code,{children:"Authorization"})," header of the response. Other implementations are still possible (especially if you use cookies)."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Note that when a new token is generated, the previous one is still valid until its expiration date."})}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"refresh-jwt.hook.ts (example)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Hook, HookDecorator, HttpResponse, isHttpResponseServerError } from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nexport function RefreshJWT(): HookDecorator {\n return Hook(ctx => {\n if (!ctx.user) {\n return;\n }\n\n return (response: HttpResponse) => {\n if (isHttpResponseServerError(response)) {\n return;\n }\n\n const newToken = sign(\n // The below object assumes that ctx.user is\n // the decoded payload (default behavior).\n {\n email: ctx.user.email,\n // id: ctx.user.id,\n // sub: ctx.user.subject,\n },\n getSecretOrPrivateKey(),\n { expiresIn: '15m' }\n );\n response.setHeader('Authorization', newToken);\n };\n\n });\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"api.controller.ts (example)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"@JWTRequired()\n@RefreshJWT()\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"make-a-database-call-to-get-more-user-properties",children:"Make a Database Call to Get More User Properties"}),"\n",(0,r.jsxs)(n.p,{children:["In several cases, the decoded payload is not sufficient. We may need to fetch extra properties from the database, such as the user permissions for example, or simply want the ",(0,r.jsx)(n.code,{children:"Context.user"})," to a be a model instance instead of a plain object."]}),"\n",(0,r.jsxs)(n.p,{children:["In these cases, the two hooks ",(0,r.jsx)(n.code,{children:"JWTRequired"})," and ",(0,r.jsx)(n.code,{children:"JWTOptional"})," offer a ",(0,r.jsx)(n.code,{children:"user"})," option to transform the decoded payload into something else. To do this,"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:["Each JSON Web Token must have a ",(0,r.jsx)(n.code,{children:"subject"})," property (or ",(0,r.jsx)(n.code,{children:"sub"}),") which is a string containing the user id. If the id is a number, it must be converted to a string using, for example, the ",(0,r.jsx)(n.code,{children:"toString()"})," method."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nconst token = sign(\n {\n // TypeScript v3.5.1 and v3.5.2 have a bug which makes the compilation fail\n // with the property \"sub\". This can be fixed by adding \"as any\" after the object.\n sub: '90485234', // Required\n id: 90485234,\n email: 'mary@foalts.org'\n },\n getSecretOrPrivateKey(),\n { expiresIn: '1h' }\n);\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:["The hook must be provided a function that takes a string id (the ",(0,r.jsx)(n.code,{children:"subject"}),") as parameter and returns the value of the ",(0,r.jsx)(n.code,{children:"Context.user"}),". If the function returns ",(0,r.jsx)(n.code,{children:"undefined"}),", the hooks returns an error ",(0,r.jsx)(n.em,{children:"401 - UNAUTHORIZED"}),"."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with TypeORM (SQL database)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nimport { User } from '../entities';\n\n@JWTRequired({ user: (id: number) => User.findOneBy({ id }) })\nexport class ApiController {\n @Get('/do-something')\n get(ctx: Context) {\n // ctx.user is the instance returned by User.findOneBy.\n // ...\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with TypeORM (MongoDB)"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\nimport { ObjectId } from 'mongodb';\n\nimport { User } from '../entities';\n\n@JWTRequired({\n userIdType: 'string',\n user: (id: string) => User.findOneBy({ _id: new ObjectId(id) }),\n})\nexport class ApiController {\n @Get('/do-something')\n get(ctx: Context) {\n // ctx.user is the instance returned by User.findOneBy.\n // ...\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with a custom function"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nconst users = [\n { id: 1, email: 'mary@foalts.org', isAdmin: true },\n { id: 2, email: 'john@foalts.org', isAdmin: false },\n];\n\nfunction getUserById(id: number) {\n return users.find(user => user.id === id);\n}\n\n@JWTRequired({ user: getUserById })\nexport class ApiController {\n @Get('/do-something')\n get(ctx: Context) {\n // ctx.user is an item of the `users` array.\n // ...\n }\n}\n"})}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"specifying-a-different-encoding-for-secrets",children:"Specifying a Different Encoding for Secrets"}),"\n",(0,r.jsxs)(n.p,{children:["By default, UTF-8 is used to encode the secret string into bytes when verifying the token. However, you can use another character encoding with the ",(0,r.jsx)(n.code,{children:"settings.jwt.secretEncoding"})," configuration key."]}),"\n",(0,r.jsxs)(n.p,{children:["Available encodings are listed ",(0,r.jsx)(n.a,{href:"https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings",children:"here"}),"."]}),"\n",(0,r.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(o.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n secret: HEwh0TW7w6a5yUwIrpHilUqetAqTFAVSHx2rg6DWNtg=\n secretEncoding: base64\n"})})}),(0,r.jsx)(o.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "HEwh0TW7w6a5yUwIrpHilUqetAqTFAVSHx2rg6DWNtg=",\n "secretEncoding": "base64",\n }\n }\n}\n'})})}),(0,r.jsx)(o.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n jwt: {\n secret: "HEwh0TW7w6a5yUwIrpHilUqetAqTFAVSHx2rg6DWNtg=",\n secretEncoding: "base64",\n }\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h3,{id:"usage-with-cookies",children:"Usage with Cookies"}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Be aware that if you use cookies, your application must provide a ",(0,r.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF defense"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["By default, the hooks expect the token to be sent in the ",(0,r.jsx)(n.strong,{children:"Authorization"})," header using the ",(0,r.jsx)(n.strong,{children:"Bearer"})," schema. But it is also possible to send the token in a cookie with the ",(0,r.jsx)(n.code,{children:"cookie"})," option."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"api.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({ cookie: true })\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"auth.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"\nimport { setAuthCookie, removeAuthCookie } from '@foal/jwt';\n\nexport class AuthController {\n\n @Post('/login')\n async login(ctx: Context) {\n // ...\n\n const response = new HttpResponseNoContent();\n // Do not forget the \"await\" keyword.\n await setAuthCookie(response, token);\n return response;\n }\n\n @Post('/logout')\n logout(ctx: Context) {\n // ...\n\n const response = new HttpResponseNoContent();\n removeAuthCookie(response);\n return response;\n }\n\n}\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Note: the cookie expire date is equal to the JWT expire date."})}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"cookie-options",children:"Cookie options"}),"\n",(0,r.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,r.jsx)(o.A,{value:"yaml",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n cookie:\n name: mycookiename # Default: auth\n domain: example.com\n httpOnly: true # Warning: unlike session tokens, the httpOnly directive has no default value.\n path: /foo # Default: /\n sameSite: strict # Default: lax if settings.jwt.csrf.enabled is true.\n secure: true\n"})})}),(0,r.jsx)(o.A,{value:"json",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "cookie": {\n "name": "mycookiename",\n "domain": "example.com",\n "httpOnly": true,\n "path": "/foo",\n "sameSite": "strict",\n "secure": true\n }\n }\n }\n}\n'})})}),(0,r.jsx)(o.A,{value:"js",children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n jwt: {\n cookie: {\n name: "mycookiename",\n domain: "example.com",\n httpOnly: true,\n path: "/foo",\n sameSite: "strict",\n secure: true\n }\n }\n }\n}\n'})})})]}),"\n",(0,r.jsx)(n.h3,{id:"use-rsa-or-ecdsa-publicprivate-keys",children:"Use RSA or ECDSA public/private keys"}),"\n",(0,r.jsx)(n.p,{children:"JWTs can also be signed using a public/private key pair using RSA or ECDSA."}),"\n",(0,r.jsx)(n.h4,{id:"provide-the-public-and-private-keys",children:"Provide the Public and Private Keys"}),"\n",(0,r.jsx)(n.p,{children:"First of all, specify in the configuration where the keys are stored."}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"config/default.js"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-javascript",children:"const { Env } = require('@foal/core');\nconst { readFileSync } = require('fs');\n\nmodule.exports = {\n settings: {\n jwt: {\n privateKey: Env.get('RSA_PRIVATE_KEY') || readFileSync('./id_rsa', 'utf8'),\n publicKey: Env.get('RSA_PUBLIC_KEY') || readFileSync('./id_rsa.pub', 'utf8'),\n }\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Then you can provide the keys in RSA files (",(0,r.jsx)(n.code,{children:"id_rsa"})," and ",(0,r.jsx)(n.code,{children:".id_rsa/pub"}),") or in environment variables."]}),"\n",(0,r.jsx)(n.h3,{id:"generate-temporary-tokens",children:"Generate Temporary Tokens"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { Config } from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nconst token = sign(\n {\n email: 'john@foalts.org'\n },\n getSecretOrPrivateKey(),\n { expiresIn: '1h', algorithm: 'RS256' }\n);\n"})}),"\n",(0,r.jsx)(n.h4,{id:"receive--verify-tokens-1",children:"Receive & Verify Tokens"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example with RSA"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({}, { algorithm: 'RS256' })\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.h3,{id:"audience-issuer-and-other-options",children:"Audience, Issuer and Other Options"}),"\n",(0,r.jsxs)(n.p,{children:["The second parameter of ",(0,r.jsx)(n.code,{children:"JWTOptional"})," and ",(0,r.jsx)(n.code,{children:"JWTRequired"})," allows to specify the required audience or issuer as well as other properties. It is passed as options to the ",(0,r.jsx)(n.code,{children:"verify"})," function of the ",(0,r.jsx)(n.a,{href:"https://www.npmjs.com/package/jsonwebtoken",children:"jsonwebtoken"})," library."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example checking the audience"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({}, { audience: [ /urn:f[o]{2}/, 'urn:bar' ] })\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example checking the issuer"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({}, { issuer: 'foo' })\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.h3,{id:"retreive-a-dynamic-secret-or-public-key",children:"Retreive a Dynamic Secret Or Public Key"}),"\n",(0,r.jsxs)(n.p,{children:["By default ",(0,r.jsx)(n.code,{children:"JWTRequired"})," and ",(0,r.jsx)(n.code,{children:"JWTOptional"})," use the value of the configuration keys ",(0,r.jsx)(n.code,{children:"settings.jwt.secret"})," or ",(0,r.jsx)(n.code,{children:"settings.jwt.publicKey"})," as a static secret (or public key)."]}),"\n",(0,r.jsxs)(n.p,{children:["But it is also possible to dynamically retrieve a key to verify the token. To do so, you can specify a function with the below signature to the ",(0,r.jsx)(n.code,{children:"secretOrPublicKey"})," option."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"(header: any, payload: any) => Promise;\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n secretOrPublicKey: async (header, payload) => {\n // ...\n return 'my-secret';\n }\n})\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If needed, this function can throw an ",(0,r.jsx)(n.code,{children:"InvalidTokenError"})," to return a 401 error to the client."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n secretOrPublicKey: async (header, payload) => {\n if (header.alg !== 'RS256') {\n throw new InvalidTokenError('invalid algorithm');\n }\n return 'my-secret';\n }\n})\nexport class ApiController {\n // ...\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["In the above example, if the algorithm specified in the token is not ",(0,r.jsx)(n.code,{children:"RS256"}),", then the server will respond a ",(0,r.jsx)(n.code,{children:"401 - UNAUTHORIZED"})," error with this content:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"{\n code: 'invalid_token',\n description: 'invalid algorithm'\n}\n"})}),"\n",(0,r.jsx)(n.h4,{id:"retreive-a-public-key-from-a-jwks-endpoint",children:"Retreive a Public Key from a JWKS endpoint"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install @foal/jwks-rsa\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"getRSAPublicKeyFromJWKS"})," allows you to retreive a public key from a JWKS endpoint. It is based on the ",(0,r.jsxs)(n.a,{href:"https://github.com/auth0/node-jwks-rsa",children:[(0,r.jsx)(n.code,{children:"jwks-rsa"})," library"]}),"."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n@JWTRequired({\n secretOrPublicKey: getRSAPublicKeyFromJWKS({\n cache: true,\n cacheMaxEntries: 5, // Default value\n cacheMaxAge: ms('10h'), // Default value\n jwksUri: 'http://localhost:3000/.well-known/jwks.json',\n })\n})\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.h3,{id:"auth0-and-aws-cognito-examples",children:"Auth0 and AWS Cognito (examples)"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npm install @foal/jwks-rsa\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:"Auth0 & AWS Cognito are both platforms to manage authentication & authorization."}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["This section provides examples on how to decode and verify JWTs generated by these platforms (the ",(0,r.jsx)(n.code,{children:"id_token"}),"). It assumes that you are already familiar with them."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Auth0"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n// These lines assume that you provided your DOMAIN and AUDIENCE in either\n// an .env file, in environment variables or in one the configuration file \n// in `config/`.\nconst domain = Config.getOrThrow('auth0.domain', 'string');\nconst audience = Config.getOrThrow('auth0.audience', 'string');\n\n@JWTRequired({\n secretOrPublicKey: getRSAPublicKeyFromJWKS({\n cache: true,\n jwksRequestsPerMinute: 5,\n jwksUri: `https://${domain}/.well-known/jwks.json`,\n rateLimit: true,\n })\n}, {\n algorithms: [ 'RS256' ],\n audience,\n issuer: `https://${domain}/`,\n})\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"AWS Cognito"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { JWTRequired } from '@foal/jwt';\n\n// These lines assume that you provided your CLIENT_ID, DOMAIN and USER_POOL_ID\n// in either an .env file, in environment variables or in one the configuration \n// file in `config/`.\nconst clientId = Config.getOrThrow('cognito.clientId', 'string');\nconst domain = Config.getOrThrow('cognito.domain', 'string');\nconst userPoolId = Config.getOrThrow('cognito.userPoolId', 'string');\n\n@JWTRequired({\n secretOrPublicKey: getRSAPublicKeyFromJWKS({\n cache: true,\n jwksRequestsPerMinute: 5,\n jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`,\n rateLimit: true,\n })\n}, {\n algorithms: [ 'RS256' ],\n audience: clientId,\n issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`,\n})\nexport class ApiController {\n // ...\n}\n\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Note: The above example does not use a secret for simplicity."})}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"hook-errors",children:"Hook Errors"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Error"}),(0,r.jsx)(n.th,{children:"Response Status"}),(0,r.jsx)(n.th,{children:"Response Body"}),(0,r.jsxs)(n.th,{children:[(0,r.jsx)(n.code,{children:"WWW-Authenticate"})," Response Header"]})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["No secret or public key is provided in ",(0,r.jsx)(n.code,{children:"default.json"})," or as environment variable."]}),(0,r.jsx)(n.td,{children:"500"}),(0,r.jsx)(n.td,{}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["The ",(0,r.jsx)(n.code,{children:"Authorization"})," header does not exist (only for ",(0,r.jsx)(n.code,{children:"JWTRequired"}),")."]}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_request', description: 'Authorization header not found.' }"})}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["The auth cookie does not exist (only for ",(0,r.jsx)(n.code,{children:"JWTRequired"}),")."]}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_request', description: 'Auth cookie not found.' }"})}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["The ",(0,r.jsx)(n.code,{children:"Authorization"})," header does use the Bearer scheme."]}),(0,r.jsx)(n.td,{children:"400"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_request', description: 'Expected a bearer token. Scheme is Authorization: Bearer .' }"})}),(0,r.jsx)(n.td,{})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The token is black listed."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt revoked' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt revoked"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The token is not a JWT."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt malformed' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt malformed"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The signature is invalid."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt signature' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt signature"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The token is expired."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt expired' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt expired"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The audience is not expected."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt audience invalid. expected: xxx' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt audience invalid. expected: xxx"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"The issuer is not expected."}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'jwt issuer invalid. expected: xxx' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="jwt issuer invalid. expected: xxx"'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["There is no subject claim and ",(0,r.jsx)(n.code,{children:"options.user"})," is defined."]}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'The token must include a subject which is the id of the user.' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="The token must include a subject which is the id of the user."'})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"options.user"})," is defined and no user was found from its value (function)."]}),(0,r.jsx)(n.td,{children:"401"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"{ code: 'invalid_token', description: 'The token subject does not match any user.' }"})}),(0,r.jsx)(n.td,{children:'error="invalid_token", error_description="The token subject does not match any user."'})]})]})]})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},19365:(e,n,t)=>{t.d(n,{A:()=>o});t(96540);var r=t(34164);const s={tabItem:"tabItem_Ymn6"};var i=t(74848);function o(e){let{children:n,hidden:t,className:o}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,r.A)(s.tabItem,o),hidden:t,children:n})}},11470:(e,n,t)=>{t.d(n,{A:()=>k});var r=t(96540),s=t(34164),i=t(23104),o=t(56347),a=t(205),l=t(57485),c=t(31682),d=t(89466);function h(e){return r.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:t}=e;return(0,r.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:t,attributes:r,default:s}}=e;return{value:n,label:t,attributes:r,default:s}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:t}=e;const s=(0,o.W6)(),i=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,l.aZ)(i),(0,r.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(s.location.search);n.set(i,e),s.replace({...s.location,search:n.toString()})}),[i,s])]}function j(e){const{defaultValue:n,queryString:t=!1,groupId:s}=e,i=u(e),[o,l]=(0,r.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const r=t.find((e=>e.default))??t[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:i}))),[c,h]=x({queryString:t,groupId:s}),[j,m]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[s,i]=(0,d.Dv)(t);return[s,(0,r.useCallback)((e=>{t&&i.set(e)}),[t,i])]}({groupId:s}),f=(()=>{const e=c??j;return p({value:e,tabValues:i})?e:null})();(0,a.A)((()=>{f&&l(f)}),[f]);return{selectedValue:o,selectValue:(0,r.useCallback)((e=>{if(!p({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);l(e),h(e),m(e)}),[h,m,i]),tabValues:i}}var m=t(92303);const f={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var g=t(74848);function v(e){let{className:n,block:t,selectedValue:r,selectValue:o,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const n=e.currentTarget,t=l.indexOf(n),s=a[t].value;s!==r&&(c(n),o(s))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=l.indexOf(e.currentTarget)+1;n=l[t]??l[0];break}case"ArrowLeft":{const t=l.indexOf(e.currentTarget)-1;n=l[t]??l[l.length-1];break}}n?.focus()};return(0,g.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":t},n),children:a.map((e=>{let{value:n,label:t,attributes:i}=e;return(0,g.jsx)("li",{role:"tab",tabIndex:r===n?0:-1,"aria-selected":r===n,ref:e=>l.push(e),onKeyDown:h,onClick:d,...i,className:(0,s.A)("tabs__item",f.tabItem,i?.className,{"tabs__item--active":r===n}),children:t??n},n)}))})}function y(e){let{lazy:n,children:t,selectedValue:s}=e;const i=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===s));return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,g.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==s})))})}function b(e){const n=j(e);return(0,g.jsxs)("div",{className:(0,s.A)("tabs-container",f.tabList),children:[(0,g.jsx)(v,{...e,...n}),(0,g.jsx)(y,{...e,...n})]})}function k(e){const n=(0,m.A)();return(0,g.jsx)(b,{...e,children:h(e.children)},String(n))}},28453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>a});var r=t(96540);const s={},i=r.createContext(s);function o(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/92999a1c.49e6f3f4.js b/assets/js/92999a1c.08475926.js similarity index 76% rename from assets/js/92999a1c.49e6f3f4.js rename to assets/js/92999a1c.08475926.js index 0fc621bf9e..035939f5b3 100644 --- a/assets/js/92999a1c.49e6f3f4.js +++ b/assets/js/92999a1c.08475926.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8790],{81116:e=>{e.exports=JSON.parse('{"permalink":"/blog/page/3","page":3,"postsPerPage":10,"totalPages":3,"totalCount":25,"previousPage":"/blog/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8790],{81116:e=>{e.exports=JSON.parse('{"permalink":"/blog/page/3","page":3,"postsPerPage":10,"totalPages":3,"totalCount":26,"previousPage":"/blog/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.3688f71d.js b/assets/js/935f2afb.3688f71d.js deleted file mode 100644 index 715a3d9e69..0000000000 --- a/assets/js/935f2afb.3688f71d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8581],{35610:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"v4","banner":null,"badge":true,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"someSidebar":[{"type":"link","label":"Introduction","href":"/docs/","docId":"README","unlisted":false},{"type":"category","label":"TUTORIALS","collapsed":false,"items":[{"type":"category","label":"Simple To-Do List","items":[{"type":"link","label":"Installation","href":"/docs/tutorials/simple-todo-list/1-installation","docId":"tutorials/simple-todo-list/tuto-1-installation","unlisted":false},{"type":"link","label":"Introduction","href":"/docs/tutorials/simple-todo-list/2-introduction","docId":"tutorials/simple-todo-list/tuto-2-introduction","unlisted":false},{"type":"link","label":"The Todo Model","href":"/docs/tutorials/simple-todo-list/3-the-todo-model","docId":"tutorials/simple-todo-list/tuto-3-the-todo-model","unlisted":false},{"type":"link","label":"The Shell Script create-todo","href":"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo","docId":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","unlisted":false},{"type":"link","label":"The REST API","href":"/docs/tutorials/simple-todo-list/5-the-rest-api","docId":"tutorials/simple-todo-list/tuto-5-the-rest-api","unlisted":false},{"type":"link","label":"Validation & Sanitization","href":"/docs/tutorials/simple-todo-list/6-validation-and-sanitization","docId":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","unlisted":false},{"type":"link","label":"Unit Testing","href":"/docs/tutorials/simple-todo-list/7-unit-testing","docId":"tutorials/simple-todo-list/tuto-7-unit-testing","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Full Example with React","items":[{"type":"link","label":"Introduction","href":"/docs/tutorials/real-world-example-with-react/1-introduction","docId":"tutorials/real-world-example-with-react/tuto-1-introduction","unlisted":false},{"type":"link","label":"Database Set Up","href":"/docs/tutorials/real-world-example-with-react/2-database-set-up","docId":"tutorials/real-world-example-with-react/tuto-2-database-set-up","unlisted":false},{"type":"link","label":"The User and Story Models","href":"/docs/tutorials/real-world-example-with-react/3-the-models","docId":"tutorials/real-world-example-with-react/tuto-3-the-models","unlisted":false},{"type":"link","label":"The Shell Scripts","href":"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts","docId":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","unlisted":false},{"type":"link","label":"Your First Route","href":"/docs/tutorials/real-world-example-with-react/5-our-first-route","docId":"tutorials/real-world-example-with-react/tuto-5-our-first-route","unlisted":false},{"type":"link","label":"API Testing with Swagger","href":"/docs/tutorials/real-world-example-with-react/6-swagger-interface","docId":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","unlisted":false},{"type":"link","label":"The Frontend App","href":"/docs/tutorials/real-world-example-with-react/7-add-frontend","docId":"tutorials/real-world-example-with-react/tuto-7-add-frontend","unlisted":false},{"type":"link","label":"Logging Users In and Out","href":"/docs/tutorials/real-world-example-with-react/8-authentication","docId":"tutorials/real-world-example-with-react/tuto-8-authentication","unlisted":false},{"type":"link","label":"Authenticating Users in the API","href":"/docs/tutorials/real-world-example-with-react/9-authenticated-api","docId":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","unlisted":false},{"type":"link","label":"Authenticating Users in React","href":"/docs/tutorials/real-world-example-with-react/10-auth-with-react","docId":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","unlisted":false},{"type":"link","label":"Adding Sign Up","href":"/docs/tutorials/real-world-example-with-react/11-sign-up","docId":"tutorials/real-world-example-with-react/tuto-11-sign-up","unlisted":false},{"type":"link","label":"Image Upload and Download","href":"/docs/tutorials/real-world-example-with-react/12-file-upload","docId":"tutorials/real-world-example-with-react/tuto-12-file-upload","unlisted":false},{"type":"link","label":"CSRF Protection","href":"/docs/tutorials/real-world-example-with-react/13-csrf","docId":"tutorials/real-world-example-with-react/tuto-13-csrf","unlisted":false},{"type":"link","label":"Production Build","href":"/docs/tutorials/real-world-example-with-react/14-production-build","docId":"tutorials/real-world-example-with-react/tuto-14-production-build","unlisted":false},{"type":"link","label":"Social Auth with Google","href":"/docs/tutorials/real-world-example-with-react/15-social-auth","docId":"tutorials/real-world-example-with-react/tuto-15-social-auth","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsible":true},{"type":"category","label":"TOPIC GUIDES","collapsed":false,"items":[{"type":"category","label":"Architecture","items":[{"type":"link","label":"Architecture Overview","href":"/docs/architecture/architecture-overview","docId":"architecture/architecture-overview","unlisted":false},{"type":"link","label":"Controllers","href":"/docs/architecture/controllers","docId":"architecture/controllers","unlisted":false},{"type":"link","label":"Services & Dependency Injection","href":"/docs/architecture/services-and-dependency-injection","docId":"architecture/services-and-dependency-injection","unlisted":false},{"type":"link","label":"Hooks","href":"/docs/architecture/hooks","docId":"architecture/hooks","unlisted":false},{"type":"link","label":"Initialization","href":"/docs/architecture/initialization","docId":"architecture/initialization","unlisted":false},{"type":"link","label":"Error Handling","href":"/docs/architecture/error-handling","docId":"architecture/error-handling","unlisted":false},{"type":"link","label":"Configuration","href":"/docs/architecture/configuration","docId":"architecture/configuration","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Common","items":[{"type":"link","label":"Validation","href":"/docs/common/validation-and-sanitization","docId":"common/validation-and-sanitization","unlisted":false},{"type":"category","label":"File Storage","items":[{"type":"link","label":"Local & Cloud Storage","href":"/docs/common/file-storage/local-and-cloud-storage","docId":"common/file-storage/local-and-cloud-storage","unlisted":false},{"type":"link","label":"Upload & Download Files","href":"/docs/common/file-storage/upload-and-download-files","docId":"common/file-storage/upload-and-download-files","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","label":"Serialization","href":"/docs/common/serialization","docId":"common/serialization","unlisted":false},{"type":"link","label":"Logging","href":"/docs/common/logging","docId":"common/logging","unlisted":false},{"type":"link","label":"Task Scheduling","href":"/docs/common/task-scheduling","docId":"common/task-scheduling","unlisted":false},{"type":"link","label":"REST API","href":"/docs/common/rest-blueprints","docId":"common/rest-blueprints","unlisted":false},{"type":"link","label":"OpenAPI","href":"/docs/common/openapi-and-swagger-ui","docId":"common/openapi-and-swagger-ui","unlisted":false},{"type":"link","label":"GraphQL","href":"/docs/common/graphql","docId":"common/graphql","unlisted":false},{"type":"link","label":"WebSockets","href":"/docs/common/websockets","docId":"common/websockets","unlisted":false},{"type":"link","label":"gRPC","href":"/docs/common/gRPC","docId":"common/gRPC","unlisted":false},{"type":"link","label":"Utilities","href":"/docs/common/utilities","docId":"common/utilities","unlisted":false},{"type":"link","label":"ExpressJS","href":"/docs/common/expressjs","docId":"common/expressjs","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Databases","items":[{"type":"category","label":"With TypeORM","items":[{"type":"link","label":"Introduction","href":"/docs/databases/typeorm/introduction","docId":"databases/typeorm/introduction","unlisted":false},{"type":"link","label":"Models & Queries","href":"/docs/databases/typeorm/create-models-and-queries","docId":"databases/typeorm/create-models-and-queries","unlisted":false},{"type":"link","label":"Migrations","href":"/docs/databases/typeorm/generate-and-run-migrations","docId":"databases/typeorm/generate-and-run-migrations","unlisted":false},{"type":"link","label":"NoSQL","href":"/docs/databases/typeorm/mongodb","docId":"databases/typeorm/mongodb","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"With another ORM","items":[{"type":"link","label":"Introduction","href":"/docs/databases/other-orm/introduction","docId":"databases/other-orm/introduction","unlisted":false},{"type":"link","label":"Prisma","href":"/docs/databases/other-orm/prisma","docId":"databases/other-orm/prisma","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsed":true,"collapsible":true},{"type":"category","label":"Authentication","items":[{"type":"link","label":"Quick Start","href":"/docs/authentication/quick-start","docId":"authentication/quick-start","unlisted":false},{"type":"link","label":"Users","href":"/docs/authentication/user-class","docId":"authentication/user-class","unlisted":false},{"type":"link","label":"Passwords","href":"/docs/authentication/password-management","docId":"authentication/password-management","unlisted":false},{"type":"link","label":"Session Tokens","href":"/docs/authentication/session-tokens","docId":"authentication/session-tokens","unlisted":false},{"type":"link","label":"JSON Web Tokens","href":"/docs/authentication/jwt","docId":"authentication/jwt","unlisted":false},{"type":"link","label":"Social Auth","href":"/docs/authentication/social-auth","docId":"authentication/social-auth","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Authorization","items":[{"type":"link","label":"Administrators & Roles","href":"/docs/authorization/administrators-and-roles","docId":"authorization/administrators-and-roles","unlisted":false},{"type":"link","label":"Groups & Permissions","href":"/docs/authorization/groups-and-permissions","docId":"authorization/groups-and-permissions","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Frontend","items":[{"type":"link","label":"Single Page Applications","href":"/docs/frontend/single-page-applications","docId":"frontend/single-page-applications","unlisted":false},{"type":"link","label":"Angular, React & Vue","href":"/docs/frontend/angular-react-vue","docId":"frontend/angular-react-vue","unlisted":false},{"type":"link","label":"Server-Side Rendering","href":"/docs/frontend/server-side-rendering","docId":"frontend/server-side-rendering","unlisted":false},{"type":"link","label":"Nuxt","href":"/docs/frontend/nuxt.js","docId":"frontend/nuxt.js","unlisted":false},{"type":"link","label":"404 Page","href":"/docs/frontend/not-found-page","docId":"frontend/not-found-page","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"CLI","items":[{"type":"link","label":"Commands","href":"/docs/cli/commands","docId":"cli/commands","unlisted":false},{"type":"link","label":"Shell Scripts","href":"/docs/cli/shell-scripts","docId":"cli/shell-scripts","unlisted":false},{"type":"link","label":"Code Generation","href":"/docs/cli/code-generation","docId":"cli/code-generation","unlisted":false},{"type":"link","label":"Linting","href":"/docs/cli/linting-and-code-style","docId":"cli/linting-and-code-style","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Testing","items":[{"type":"link","label":"Introduction","href":"/docs/testing/introduction","docId":"testing/introduction","unlisted":false},{"type":"link","label":"Unit Testing","href":"/docs/testing/unit-testing","docId":"testing/unit-testing","unlisted":false},{"type":"link","label":"E2E Testing","href":"/docs/testing/e2e-testing","docId":"testing/e2e-testing","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Security","items":[{"type":"link","label":"Response Headers","href":"/docs/security/http-headers-protection","docId":"security/http-headers-protection","unlisted":false},{"type":"link","label":"CSRF","href":"/docs/security/csrf-protection","docId":"security/csrf-protection","unlisted":false},{"type":"link","label":"CORS","href":"/docs/security/cors","docId":"security/cors","unlisted":false},{"type":"link","label":"Rate Limiting","href":"/docs/security/rate-limiting","docId":"security/rate-limiting","unlisted":false},{"type":"link","label":"Body Size Limiting","href":"/docs/security/body-size-limiting","docId":"security/body-size-limiting","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Deployment","items":[{"type":"link","label":"Checklist","href":"/docs/deployment-and-environments/checklist","docId":"deployment-and-environments/checklist","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsible":true},{"type":"category","label":"Comparison with Other Frameworks","collapsed":false,"items":[{"type":"link","label":"Express / Fastify","href":"/docs/comparison-with-other-frameworks/express-fastify","docId":"comparison-with-other-frameworks/express-fastify","unlisted":false}],"collapsible":true},{"type":"category","label":"Upgrading","collapsed":false,"items":[{"type":"link","label":"To v4","href":"https://github.com/FoalTS/foal/releases/tag/v4.0.0"},{"type":"link","label":"To v3","href":"https://foalts.org/docs/3.x/upgrade-to-v3/"},{"type":"link","label":"To v2","href":"https://foalts.org/docs/2.x/upgrade-to-v2/"},{"type":"link","label":"To v1","href":"https://github.com/FoalTS/foal/releases/tag/v1.0.0"}],"collapsible":true},{"type":"category","label":"Community","collapsed":false,"items":[{"type":"link","label":"Awesome Foal","href":"/docs/community/awesome-foal","docId":"community/awesome-foal","unlisted":false}],"collapsible":true}]},"docs":{"architecture/architecture-overview":{"id":"architecture/architecture-overview","title":"Architecture Overview","description":"FoalTS is a framework for creating server-side Node.js applications. It is written in TypeScript, a typed superset of JavaScript that provides advanced development tools and the latest language features.","sidebar":"someSidebar"},"architecture/configuration":{"id":"architecture/configuration","title":"Configuration","description":"In FoalTS, configuration refers to any parameter that may vary between deploy environments (production, development, test, etc). It includes sensitive information, such as your database credentials, or simple settings, such as the server port.","sidebar":"someSidebar"},"architecture/controllers":{"id":"architecture/controllers","title":"Controllers","description":"Description","sidebar":"someSidebar"},"architecture/error-handling":{"id":"architecture/error-handling","title":"Error Handling","description":"When creating a new project with Foal, error handling is already configured for you. When an error is thrown or rejected in a controller or a hook, the application returns an HTML page Internal Server Error with the status code 500. If the configuration parameter settings.debug is set to true (which is the case during development or testing), the page includes some details about the error (name, message, stack trace, etc).","sidebar":"someSidebar"},"architecture/hooks":{"id":"architecture/hooks","title":"Hooks","description":"Description","sidebar":"someSidebar"},"architecture/initialization":{"id":"architecture/initialization","title":"Initialization","description":"In many situations, we need to initialize the application (i.e perform certain actions) before listening to incoming HTTP requests. This is the case, for example, if you need to establish a connection to the database.","sidebar":"someSidebar"},"architecture/services-and-dependency-injection":{"id":"architecture/services-and-dependency-injection","title":"Services & Dependency Injection","description":"Description","sidebar":"someSidebar"},"authentication/jwt":{"id":"authentication/jwt","title":"Authentication with JWT","description":"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.","sidebar":"someSidebar"},"authentication/password-management":{"id":"authentication/password-management","title":"Password Management","description":"Passwords must never be stored in the database in plain text. If they were and attackers were to gain access to the database, all passwords would be compromised. To prevent this, they must be hashed and salted and their hashes stored. Foal provides two functions for this purpose.","sidebar":"someSidebar"},"authentication/quick-start":{"id":"authentication/quick-start","title":"Quick Start","description":"Authentication is the process of verifying that a user is who he or she claims to be. It answers the question Who is the user?.","sidebar":"someSidebar"},"authentication/session-tokens":{"id":"authentication/session-tokens","title":"Session Tokens","description":"Introduction","sidebar":"someSidebar"},"authentication/social-auth":{"id":"authentication/social-auth","title":"Social Authentication","description":"In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:","sidebar":"someSidebar"},"authentication/user-class":{"id":"authentication/user-class","title":"Users","description":"The User Entity","sidebar":"someSidebar"},"authorization/administrators-and-roles":{"id":"authorization/administrators-and-roles","title":"Administrators and Roles","description":"In simple applications, access control can be managed with static roles or even with an isAdmin column in the simplest cases.","sidebar":"someSidebar"},"authorization/groups-and-permissions":{"id":"authorization/groups-and-permissions","title":"Groups and Permissions","description":"In advanced applications, access control can be managed through permissions and groups.","sidebar":"someSidebar"},"cli/code-generation":{"id":"cli/code-generation","title":"Code Generation","description":"Create a project","sidebar":"someSidebar"},"cli/commands":{"id":"cli/commands","title":"Commands","description":"FoalTS provides several commands to help you build and develop your app.","sidebar":"someSidebar"},"cli/linting-and-code-style":{"id":"cli/linting-and-code-style","title":"Linting and Code Style","description":"A linter is a tool that analizes source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. In particular, it helps teams to keep the code consistent between their members.","sidebar":"someSidebar"},"cli/shell-scripts":{"id":"cli/shell-scripts","title":"Shell Scripts","description":"Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play.","sidebar":"someSidebar"},"common/expressjs":{"id":"common/expressjs","title":"ExpressJS","description":"FoalTS applications are created with the createApp function in the src/index.ts file. This function takes the root controller class (known as AppController) as parameter.","sidebar":"someSidebar"},"common/file-storage/local-and-cloud-storage":{"id":"common/file-storage/local-and-cloud-storage","title":"Local and Cloud Storage","description":"FoalTS provides its own file system for reading, writing and deleting files locally or in the Cloud. Thanks to its unified interface, you can easily choose different storage for each of your environments. This is especially useful when you\'re moving from development to production.","sidebar":"someSidebar"},"common/file-storage/upload-and-download-files":{"id":"common/file-storage/upload-and-download-files","title":"Upload and Download Files","description":"Files can be uploaded and downloaded using FoalTS file system. It allows you to use different types of file storage such as the local file system or cloud storage.","sidebar":"someSidebar"},"common/graphql":{"id":"common/graphql","title":"GraphQL","description":"GraphQL is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for.","sidebar":"someSidebar"},"common/gRPC":{"id":"common/gRPC","title":"gRPC","description":"gRPC is a Remote Procedure Call (RPC) framework that can run in any environment. It can connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.","sidebar":"someSidebar"},"common/logging":{"id":"common/logging","title":"Logging","description":"Foal provides an advanced built-in logger. This page shows how to use it.","sidebar":"someSidebar"},"common/openapi-and-swagger-ui":{"id":"common/openapi-and-swagger-ui","title":"OpenAPI & Swagger UI","description":"Introduction","sidebar":"someSidebar"},"common/rest-blueprints":{"id":"common/rest-blueprints","title":"REST API","description":"Example:","sidebar":"someSidebar"},"common/serialization":{"id":"common/serialization","title":"Serialization","description":"This document shows how to serialize class instances into plain objects and, conversely, how to deserialize plain objects into class instances. It is based on the class-transformer library.","sidebar":"someSidebar"},"common/task-scheduling":{"id":"common/task-scheduling","title":"Task Scheduling","description":"You can schedule jobs using shell scripts and the node-schedule library.","sidebar":"someSidebar"},"common/utilities":{"id":"common/utilities","title":"Utilities","description":"Random Tokens","sidebar":"someSidebar"},"common/validation-and-sanitization":{"id":"common/validation-and-sanitization","title":"Validation & Sanitization","description":"Validation checks if an input meets a set of criteria (such as the value of a property is a string).","sidebar":"someSidebar"},"common/websockets":{"id":"common/websockets","title":"Real-Time Communication","description":"Foal allows you to establish two-way interactive communication between your server(s) and your clients. For this, it uses the socket.io v4 library which is primarily based on the WebSocket protocol. It supports disconnection detection and automatic reconnection and works with proxies and load balancers.","sidebar":"someSidebar"},"community/awesome-foal":{"id":"community/awesome-foal","title":"Awesome Foal","description":"A collection of official and unofficial libraries and resources about Foal.","sidebar":"someSidebar"},"comparison-with-other-frameworks/express-fastify":{"id":"comparison-with-other-frameworks/express-fastify","title":"Foal vs Express or Fastify","description":"These pages are definitely the most difficult to write. If you are here, it is probably because you want to know if you should choose Foal over another framework. There are many in the Node ecosystem and choosing one is not always an easy task. These pages are meant to help you on your way.","sidebar":"someSidebar"},"databases/other-orm/introduction":{"id":"databases/other-orm/introduction","title":"Using Another ORM","description":"The core of the framework is independent of TypeORM. So, if you do not want to use an ORM at all or use another ORM or ODM than TypeORM, you absolutely can.","sidebar":"someSidebar"},"databases/other-orm/prisma":{"id":"databases/other-orm/prisma","title":"Prisma","description":"This document shows how to configure Prisma in a FoalTS project. It assumes that you have already uninstalled TypeORM.","sidebar":"someSidebar"},"databases/typeorm/create-models-and-queries":{"id":"databases/typeorm/create-models-and-queries","title":"Models & Queries","description":"Entities","sidebar":"someSidebar"},"databases/typeorm/generate-and-run-migrations":{"id":"databases/typeorm/generate-and-run-migrations","title":"Generate & Run Migrations","description":"Database migrations are a way of propagating changes you make to your entities into your database schema. The changes you make to your models (adding a field, deleting an entity, etc.) do not automatically modify your database. You have to do it yourself.","sidebar":"someSidebar"},"databases/typeorm/introduction":{"id":"databases/typeorm/introduction","title":"TypeORM","description":"A simple model:","sidebar":"someSidebar"},"databases/typeorm/mongodb":{"id":"databases/typeorm/mongodb","title":"MongoDB (noSQL)","description":"Creating a new project","sidebar":"someSidebar"},"deployment-and-environments/checklist":{"id":"deployment-and-environments/checklist","title":"Deployment Checklist","description":"Set the Node.JS environment to production","sidebar":"someSidebar"},"frontend/angular-react-vue":{"id":"frontend/angular-react-vue","title":"Angular, React & Vue","description":"Angular, React and Vue all provide powerful CLIs for creating frontend applications. These tools are widely used, regularly improved and extensively documented. That\'s why Foal CLI do not provide ready-made features to build the frontend in their place.","sidebar":"someSidebar"},"frontend/not-found-page":{"id":"frontend/not-found-page","title":"404 Page","description":"Here\'s a way to implement custom 404 pages.","sidebar":"someSidebar"},"frontend/nuxt.js":{"id":"frontend/nuxt.js","title":"Nuxt","description":"Nuxt is a frontend framework based on Vue.JS.","sidebar":"someSidebar"},"frontend/server-side-rendering":{"id":"frontend/server-side-rendering","title":"Server-Side Rendering","description":"Regular Templates","sidebar":"someSidebar"},"frontend/single-page-applications":{"id":"frontend/single-page-applications","title":"Single Page Applications (SPA)","description":"Single-Page Applications are Web Applications that are loaded once upon the first request(s) to the backend. After retreiving all the necessary code from the server, the current page is rendered and updated directly in the browser without asking the server to render new pages. During its lifecycle, the application usually communicates with the server by making API calls to fetch, create, update or delete data. This is a common pattern used when creating a new application with Angular, React or Vue.","sidebar":"someSidebar"},"README":{"id":"README","title":"Introduction","description":"License: MIT","sidebar":"someSidebar"},"security/body-size-limiting":{"id":"security/body-size-limiting","title":"Body Size Limiting","description":"By default, FoalTS only accepts request bodies lower than 100kb. This value can be increased by using the configuration key settings.bodyParser.limit. If a number is provided, then the value specifies the number of bytes. If it is a string, the value is passed to the bytes library for parsing.","sidebar":"someSidebar"},"security/cors":{"id":"security/cors","title":"CORS Requests","description":"Building a public API requires to allow Cross-Origin Request Sharing.","sidebar":"someSidebar"},"security/csrf-protection":{"id":"security/csrf-protection","title":"CSRF Protection","description":"Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious web site, email, blog, instant message, or program causes a user\u2019s web browser to perform an unwanted action on a trusted site when the user is authenticated.","sidebar":"someSidebar"},"security/http-headers-protection":{"id":"security/http-headers-protection","title":"Protection HTTP Headers","description":"To protect the application against some common attacks, FoalTS sets by default various HTTP headers. These can be overrided in the HttpResponse objects.","sidebar":"someSidebar"},"security/rate-limiting":{"id":"security/rate-limiting","title":"Rate Limiting","description":"To avoid brute force attacks or overloading your application, you can set up a rate limiter to limit the number of requests a user can send to your application.","sidebar":"someSidebar"},"testing/e2e-testing":{"id":"testing/e2e-testing","title":"E2E Testing","description":"End-to-end tests are located in the src/e2e directory.","sidebar":"someSidebar"},"testing/introduction":{"id":"testing/introduction","title":"Introduction","description":"Every shipped app should come with a minimum set of tests. Writing tests lets you find problems early, facilitate changes and document your code. FoalTS is designed to be easily testable and provides the tools you need to write tests right away.","sidebar":"someSidebar"},"testing/unit-testing":{"id":"testing/unit-testing","title":"Unit Testing","description":"Convention","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-1-introduction":{"id":"tutorials/real-world-example-with-react/tuto-1-introduction","title":"Introduction","description":"This tutorial shows how to build a real-world application with React and Foal. It assumes that you have already read the first guide How to build a Simple To-Do List and that you have a basic knowledge of React.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-10-auth-with-react":{"id":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","title":"Authenticating Users in React","description":"The backend API is ready to be used. Now let\'s add authentication in the frontend side.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-11-sign-up":{"id":"tutorials/real-world-example-with-react/tuto-11-sign-up","title":"Adding Sign Up","description":"So far, only users created with the create-user script can log in and publish stories. In this section you will add a new API endpoint for users to sign up with the registration page.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-12-file-upload":{"id":"tutorials/real-world-example-with-react/tuto-12-file-upload","title":"Image Upload and Download","description":"The next step in this tutorial is to allow users to upload a profile picture. This image will be displayed on the homepage in front of each author\'s story.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-13-csrf":{"id":"tutorials/real-world-example-with-react/tuto-13-csrf","title":"CSRF Protection","description":"Since you use authentication with cookies, you need to add CSRF protection to your application. This is really easy with Foal, even when building a SPA.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-14-production-build":{"id":"tutorials/real-world-example-with-react/tuto-14-production-build","title":"Production Build","description":"So far, the front-end and back-end applications are compiled and served by two different development servers. The next step is to build them into a single one ready for production.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-15-social-auth":{"id":"tutorials/real-world-example-with-react/tuto-15-social-auth","title":"Social Auth with Google","description":"In this last part of the tutorial, we will give users the ability to log in with Google. Currently, they can only log in with an email and a password.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-2-database-set-up":{"id":"tutorials/real-world-example-with-react/tuto-2-database-set-up","title":"Database Set Up","description":"The first step in this tutorial is to establish a database connection. If you haven\'t already done so, install MySQL or PostgreSQL.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-3-the-models":{"id":"tutorials/real-world-example-with-react/tuto-3-the-models","title":"The User and Story Models","description":"Now that the database connection is established, you can create your two entities User and Story.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts":{"id":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","title":"The Shell Scripts","description":"Your models are ready to be used. As in the previous tutorial, you will use shell scripts to feed the database.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-5-our-first-route":{"id":"tutorials/real-world-example-with-react/tuto-5-our-first-route","title":"Your First Route","description":"The database is now filled with some stories. Let\'s implement the first route to retrieve them.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-6-swagger-interface":{"id":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","title":"API Testing with Swagger","description":"Now that the first API endpoint has been implemented, the next step is to test it.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-7-add-frontend":{"id":"tutorials/real-world-example-with-react/tuto-7-add-frontend","title":"The Frontend App","description":"Very good, so far you have a first working version of your API. It\'s time to add the frontend.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-8-authentication":{"id":"tutorials/real-world-example-with-react/tuto-8-authentication","title":"Logging Users In and Out","description":"Stories are displayed on the home page. If we want users to be able to post new stories and upload a profile picture, we need to allow them to log in to the application.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-9-authenticated-api":{"id":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","title":"Authenticating Users in the API","description":"Now that the login is configured, you can add two new routes to create and delete stories. Their access will be limited to authenticated users.","sidebar":"someSidebar"},"tutorials/simple-todo-list/installation-troubleshooting":{"id":"tutorials/simple-todo-list/installation-troubleshooting","title":"Installation Troubleshooting","description":"Errors with node-gyp"},"tutorials/simple-todo-list/tuto-1-installation":{"id":"tutorials/simple-todo-list/tuto-1-installation","title":"Installation","description":"In this tutorial you will learn how to create a basic web application with FoalTS. The demo application is a simple to-do list with which users can view, create and delete their tasks.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-2-introduction":{"id":"tutorials/simple-todo-list/tuto-2-introduction","title":"Introduction","description":"The application that you will create is a simple to-do list. It consists of a frontend part that has already been written for you and a backend part that will be the topic of this tutorial.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-3-the-todo-model":{"id":"tutorials/simple-todo-list/tuto-3-the-todo-model","title":"The Todo Model","description":"The next step is to take care of the database. By default, every new project in FoalTS is configured to use an SQLite database as it does not require any additional installation.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo":{"id":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","title":"The Shell Script create-todo","description":"Now it is time to populate the database with some tasks.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-5-the-rest-api":{"id":"tutorials/simple-todo-list/tuto-5-the-rest-api","title":"The REST API","description":"Good, so far you have a frontend working properly and some todos in your database. Now it is time to code a REST API to link them both.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-6-validation-and-sanitization":{"id":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","title":"Validation & Sanitization","description":"Currently inputs received by the server are not checked. Everyone could send anything when requesting POST /api/todos. That\'s why client inputs cannot be trusted.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-7-unit-testing":{"id":"tutorials/simple-todo-list/tuto-7-unit-testing","title":"Unit Testing","description":"The last step of this tutorial is to add some unit tests to the ApiController.","sidebar":"someSidebar"}}}')}}]); \ No newline at end of file diff --git a/assets/js/935f2afb.972950c8.js b/assets/js/935f2afb.972950c8.js new file mode 100644 index 0000000000..3b26ee070b --- /dev/null +++ b/assets/js/935f2afb.972950c8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8581],{35610:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"v4","banner":null,"badge":true,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"someSidebar":[{"type":"link","label":"Introduction","href":"/docs/","docId":"README","unlisted":false},{"type":"category","label":"TUTORIALS","collapsed":false,"items":[{"type":"category","label":"Simple To-Do List","items":[{"type":"link","label":"Installation","href":"/docs/tutorials/simple-todo-list/1-installation","docId":"tutorials/simple-todo-list/tuto-1-installation","unlisted":false},{"type":"link","label":"Introduction","href":"/docs/tutorials/simple-todo-list/2-introduction","docId":"tutorials/simple-todo-list/tuto-2-introduction","unlisted":false},{"type":"link","label":"The Todo Model","href":"/docs/tutorials/simple-todo-list/3-the-todo-model","docId":"tutorials/simple-todo-list/tuto-3-the-todo-model","unlisted":false},{"type":"link","label":"The Shell Script create-todo","href":"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo","docId":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","unlisted":false},{"type":"link","label":"The REST API","href":"/docs/tutorials/simple-todo-list/5-the-rest-api","docId":"tutorials/simple-todo-list/tuto-5-the-rest-api","unlisted":false},{"type":"link","label":"Validation & Sanitization","href":"/docs/tutorials/simple-todo-list/6-validation-and-sanitization","docId":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","unlisted":false},{"type":"link","label":"Unit Testing","href":"/docs/tutorials/simple-todo-list/7-unit-testing","docId":"tutorials/simple-todo-list/tuto-7-unit-testing","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Full Example with React","items":[{"type":"link","label":"Introduction","href":"/docs/tutorials/real-world-example-with-react/1-introduction","docId":"tutorials/real-world-example-with-react/tuto-1-introduction","unlisted":false},{"type":"link","label":"Database Set Up","href":"/docs/tutorials/real-world-example-with-react/2-database-set-up","docId":"tutorials/real-world-example-with-react/tuto-2-database-set-up","unlisted":false},{"type":"link","label":"The User and Story Models","href":"/docs/tutorials/real-world-example-with-react/3-the-models","docId":"tutorials/real-world-example-with-react/tuto-3-the-models","unlisted":false},{"type":"link","label":"The Shell Scripts","href":"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts","docId":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","unlisted":false},{"type":"link","label":"Your First Route","href":"/docs/tutorials/real-world-example-with-react/5-our-first-route","docId":"tutorials/real-world-example-with-react/tuto-5-our-first-route","unlisted":false},{"type":"link","label":"API Testing with Swagger","href":"/docs/tutorials/real-world-example-with-react/6-swagger-interface","docId":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","unlisted":false},{"type":"link","label":"The Frontend App","href":"/docs/tutorials/real-world-example-with-react/7-add-frontend","docId":"tutorials/real-world-example-with-react/tuto-7-add-frontend","unlisted":false},{"type":"link","label":"Logging Users In and Out","href":"/docs/tutorials/real-world-example-with-react/8-authentication","docId":"tutorials/real-world-example-with-react/tuto-8-authentication","unlisted":false},{"type":"link","label":"Authenticating Users in the API","href":"/docs/tutorials/real-world-example-with-react/9-authenticated-api","docId":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","unlisted":false},{"type":"link","label":"Authenticating Users in React","href":"/docs/tutorials/real-world-example-with-react/10-auth-with-react","docId":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","unlisted":false},{"type":"link","label":"Adding Sign Up","href":"/docs/tutorials/real-world-example-with-react/11-sign-up","docId":"tutorials/real-world-example-with-react/tuto-11-sign-up","unlisted":false},{"type":"link","label":"Image Upload and Download","href":"/docs/tutorials/real-world-example-with-react/12-file-upload","docId":"tutorials/real-world-example-with-react/tuto-12-file-upload","unlisted":false},{"type":"link","label":"CSRF Protection","href":"/docs/tutorials/real-world-example-with-react/13-csrf","docId":"tutorials/real-world-example-with-react/tuto-13-csrf","unlisted":false},{"type":"link","label":"Production Build","href":"/docs/tutorials/real-world-example-with-react/14-production-build","docId":"tutorials/real-world-example-with-react/tuto-14-production-build","unlisted":false},{"type":"link","label":"Social Auth with Google","href":"/docs/tutorials/real-world-example-with-react/15-social-auth","docId":"tutorials/real-world-example-with-react/tuto-15-social-auth","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsible":true},{"type":"category","label":"TOPIC GUIDES","collapsed":false,"items":[{"type":"category","label":"Architecture","items":[{"type":"link","label":"Architecture Overview","href":"/docs/architecture/architecture-overview","docId":"architecture/architecture-overview","unlisted":false},{"type":"link","label":"Controllers","href":"/docs/architecture/controllers","docId":"architecture/controllers","unlisted":false},{"type":"link","label":"Services & Dependency Injection","href":"/docs/architecture/services-and-dependency-injection","docId":"architecture/services-and-dependency-injection","unlisted":false},{"type":"link","label":"Hooks","href":"/docs/architecture/hooks","docId":"architecture/hooks","unlisted":false},{"type":"link","label":"Initialization","href":"/docs/architecture/initialization","docId":"architecture/initialization","unlisted":false},{"type":"link","label":"Error Handling","href":"/docs/architecture/error-handling","docId":"architecture/error-handling","unlisted":false},{"type":"link","label":"Configuration","href":"/docs/architecture/configuration","docId":"architecture/configuration","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Common","items":[{"type":"link","label":"Validation","href":"/docs/common/validation-and-sanitization","docId":"common/validation-and-sanitization","unlisted":false},{"type":"category","label":"File Storage","items":[{"type":"link","label":"Local & Cloud Storage","href":"/docs/common/file-storage/local-and-cloud-storage","docId":"common/file-storage/local-and-cloud-storage","unlisted":false},{"type":"link","label":"Upload & Download Files","href":"/docs/common/file-storage/upload-and-download-files","docId":"common/file-storage/upload-and-download-files","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","label":"Serialization","href":"/docs/common/serialization","docId":"common/serialization","unlisted":false},{"type":"link","label":"Logging","href":"/docs/common/logging","docId":"common/logging","unlisted":false},{"type":"link","label":"Async tasks","href":"/docs/common/async-tasks","docId":"common/async-tasks","unlisted":false},{"type":"link","label":"REST API","href":"/docs/common/rest-blueprints","docId":"common/rest-blueprints","unlisted":false},{"type":"link","label":"OpenAPI","href":"/docs/common/openapi-and-swagger-ui","docId":"common/openapi-and-swagger-ui","unlisted":false},{"type":"link","label":"GraphQL","href":"/docs/common/graphql","docId":"common/graphql","unlisted":false},{"type":"link","label":"WebSockets","href":"/docs/common/websockets","docId":"common/websockets","unlisted":false},{"type":"link","label":"gRPC","href":"/docs/common/gRPC","docId":"common/gRPC","unlisted":false},{"type":"link","label":"Utilities","href":"/docs/common/utilities","docId":"common/utilities","unlisted":false},{"type":"link","label":"ExpressJS","href":"/docs/common/expressjs","docId":"common/expressjs","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Databases","items":[{"type":"category","label":"With TypeORM","items":[{"type":"link","label":"Introduction","href":"/docs/databases/typeorm/introduction","docId":"databases/typeorm/introduction","unlisted":false},{"type":"link","label":"Models & Queries","href":"/docs/databases/typeorm/create-models-and-queries","docId":"databases/typeorm/create-models-and-queries","unlisted":false},{"type":"link","label":"Migrations","href":"/docs/databases/typeorm/generate-and-run-migrations","docId":"databases/typeorm/generate-and-run-migrations","unlisted":false},{"type":"link","label":"NoSQL","href":"/docs/databases/typeorm/mongodb","docId":"databases/typeorm/mongodb","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"With another ORM","items":[{"type":"link","label":"Introduction","href":"/docs/databases/other-orm/introduction","docId":"databases/other-orm/introduction","unlisted":false},{"type":"link","label":"Prisma","href":"/docs/databases/other-orm/prisma","docId":"databases/other-orm/prisma","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsed":true,"collapsible":true},{"type":"category","label":"Authentication","items":[{"type":"link","label":"Quick Start","href":"/docs/authentication/quick-start","docId":"authentication/quick-start","unlisted":false},{"type":"link","label":"Users","href":"/docs/authentication/user-class","docId":"authentication/user-class","unlisted":false},{"type":"link","label":"Passwords","href":"/docs/authentication/password-management","docId":"authentication/password-management","unlisted":false},{"type":"link","label":"Session Tokens","href":"/docs/authentication/session-tokens","docId":"authentication/session-tokens","unlisted":false},{"type":"link","label":"JSON Web Tokens","href":"/docs/authentication/jwt","docId":"authentication/jwt","unlisted":false},{"type":"link","label":"Social Auth","href":"/docs/authentication/social-auth","docId":"authentication/social-auth","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Authorization","items":[{"type":"link","label":"Administrators & Roles","href":"/docs/authorization/administrators-and-roles","docId":"authorization/administrators-and-roles","unlisted":false},{"type":"link","label":"Groups & Permissions","href":"/docs/authorization/groups-and-permissions","docId":"authorization/groups-and-permissions","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Frontend","items":[{"type":"link","label":"Single Page Applications","href":"/docs/frontend/single-page-applications","docId":"frontend/single-page-applications","unlisted":false},{"type":"link","label":"Angular, React & Vue","href":"/docs/frontend/angular-react-vue","docId":"frontend/angular-react-vue","unlisted":false},{"type":"link","label":"Server-Side Rendering","href":"/docs/frontend/server-side-rendering","docId":"frontend/server-side-rendering","unlisted":false},{"type":"link","label":"Nuxt","href":"/docs/frontend/nuxt.js","docId":"frontend/nuxt.js","unlisted":false},{"type":"link","label":"404 Page","href":"/docs/frontend/not-found-page","docId":"frontend/not-found-page","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"CLI","items":[{"type":"link","label":"Commands","href":"/docs/cli/commands","docId":"cli/commands","unlisted":false},{"type":"link","label":"Shell Scripts","href":"/docs/cli/shell-scripts","docId":"cli/shell-scripts","unlisted":false},{"type":"link","label":"Code Generation","href":"/docs/cli/code-generation","docId":"cli/code-generation","unlisted":false},{"type":"link","label":"Linting","href":"/docs/cli/linting-and-code-style","docId":"cli/linting-and-code-style","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Testing","items":[{"type":"link","label":"Introduction","href":"/docs/testing/introduction","docId":"testing/introduction","unlisted":false},{"type":"link","label":"Unit Testing","href":"/docs/testing/unit-testing","docId":"testing/unit-testing","unlisted":false},{"type":"link","label":"E2E Testing","href":"/docs/testing/e2e-testing","docId":"testing/e2e-testing","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Security","items":[{"type":"link","label":"Response Headers","href":"/docs/security/http-headers-protection","docId":"security/http-headers-protection","unlisted":false},{"type":"link","label":"CSRF","href":"/docs/security/csrf-protection","docId":"security/csrf-protection","unlisted":false},{"type":"link","label":"CORS","href":"/docs/security/cors","docId":"security/cors","unlisted":false},{"type":"link","label":"Rate Limiting","href":"/docs/security/rate-limiting","docId":"security/rate-limiting","unlisted":false},{"type":"link","label":"Body Size Limiting","href":"/docs/security/body-size-limiting","docId":"security/body-size-limiting","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Deployment","items":[{"type":"link","label":"Checklist","href":"/docs/deployment-and-environments/checklist","docId":"deployment-and-environments/checklist","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsible":true},{"type":"category","label":"Comparison with Other Frameworks","collapsed":false,"items":[{"type":"link","label":"Express / Fastify","href":"/docs/comparison-with-other-frameworks/express-fastify","docId":"comparison-with-other-frameworks/express-fastify","unlisted":false}],"collapsible":true},{"type":"category","label":"Upgrading","collapsed":false,"items":[{"type":"link","label":"To v4","href":"https://github.com/FoalTS/foal/releases/tag/v4.0.0"},{"type":"link","label":"To v3","href":"https://foalts.org/docs/3.x/upgrade-to-v3/"},{"type":"link","label":"To v2","href":"https://foalts.org/docs/2.x/upgrade-to-v2/"},{"type":"link","label":"To v1","href":"https://github.com/FoalTS/foal/releases/tag/v1.0.0"}],"collapsible":true},{"type":"category","label":"Community","collapsed":false,"items":[{"type":"link","label":"Awesome Foal","href":"/docs/community/awesome-foal","docId":"community/awesome-foal","unlisted":false}],"collapsible":true}]},"docs":{"architecture/architecture-overview":{"id":"architecture/architecture-overview","title":"Architecture Overview","description":"FoalTS is a framework for creating server-side Node.js applications. It is written in TypeScript, a typed superset of JavaScript that provides advanced development tools and the latest language features.","sidebar":"someSidebar"},"architecture/configuration":{"id":"architecture/configuration","title":"Configuration","description":"In FoalTS, configuration refers to any parameter that may vary between deploy environments (production, development, test, etc). It includes sensitive information, such as your database credentials, or simple settings, such as the server port.","sidebar":"someSidebar"},"architecture/controllers":{"id":"architecture/controllers","title":"Controllers","description":"Description","sidebar":"someSidebar"},"architecture/error-handling":{"id":"architecture/error-handling","title":"Error Handling","description":"When creating a new project with Foal, error handling is already configured for you. When an error is thrown or rejected in a controller or a hook, the application returns an HTML page Internal Server Error with the status code 500. If the configuration parameter settings.debug is set to true (which is the case during development or testing), the page includes some details about the error (name, message, stack trace, etc).","sidebar":"someSidebar"},"architecture/hooks":{"id":"architecture/hooks","title":"Hooks","description":"Description","sidebar":"someSidebar"},"architecture/initialization":{"id":"architecture/initialization","title":"Initialization","description":"In many situations, we need to initialize the application (i.e perform certain actions) before listening to incoming HTTP requests. This is the case, for example, if you need to establish a connection to the database.","sidebar":"someSidebar"},"architecture/services-and-dependency-injection":{"id":"architecture/services-and-dependency-injection","title":"Services & Dependency Injection","description":"Description","sidebar":"someSidebar"},"authentication/jwt":{"id":"authentication/jwt","title":"Authentication with JWT","description":"JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.","sidebar":"someSidebar"},"authentication/password-management":{"id":"authentication/password-management","title":"Password Management","description":"Passwords must never be stored in the database in plain text. If they were and attackers were to gain access to the database, all passwords would be compromised. To prevent this, they must be hashed and salted and their hashes stored. Foal provides two functions for this purpose.","sidebar":"someSidebar"},"authentication/quick-start":{"id":"authentication/quick-start","title":"Quick Start","description":"Authentication is the process of verifying that a user is who he or she claims to be. It answers the question Who is the user?.","sidebar":"someSidebar"},"authentication/session-tokens":{"id":"authentication/session-tokens","title":"Session Tokens","description":"Introduction","sidebar":"someSidebar"},"authentication/social-auth":{"id":"authentication/social-auth","title":"Social Authentication","description":"In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:","sidebar":"someSidebar"},"authentication/user-class":{"id":"authentication/user-class","title":"Users","description":"The User Entity","sidebar":"someSidebar"},"authorization/administrators-and-roles":{"id":"authorization/administrators-and-roles","title":"Administrators and Roles","description":"In simple applications, access control can be managed with static roles or even with an isAdmin column in the simplest cases.","sidebar":"someSidebar"},"authorization/groups-and-permissions":{"id":"authorization/groups-and-permissions","title":"Groups and Permissions","description":"In advanced applications, access control can be managed through permissions and groups.","sidebar":"someSidebar"},"cli/code-generation":{"id":"cli/code-generation","title":"Code Generation","description":"Create a project","sidebar":"someSidebar"},"cli/commands":{"id":"cli/commands","title":"Commands","description":"FoalTS provides several commands to help you build and develop your app.","sidebar":"someSidebar"},"cli/linting-and-code-style":{"id":"cli/linting-and-code-style","title":"Linting and Code Style","description":"A linter is a tool that analizes source code to flag programming errors, bugs, stylistic errors, and suspicious constructs. In particular, it helps teams to keep the code consistent between their members.","sidebar":"someSidebar"},"cli/shell-scripts":{"id":"cli/shell-scripts","title":"Shell Scripts","description":"Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play.","sidebar":"someSidebar"},"common/async-tasks":{"id":"common/async-tasks","title":"Async tasks","description":"Running an asynchronous task","sidebar":"someSidebar"},"common/expressjs":{"id":"common/expressjs","title":"ExpressJS","description":"FoalTS applications are created with the createApp function in the src/index.ts file. This function takes the root controller class (known as AppController) as parameter.","sidebar":"someSidebar"},"common/file-storage/local-and-cloud-storage":{"id":"common/file-storage/local-and-cloud-storage","title":"Local and Cloud Storage","description":"FoalTS provides its own file system for reading, writing and deleting files locally or in the Cloud. Thanks to its unified interface, you can easily choose different storage for each of your environments. This is especially useful when you\'re moving from development to production.","sidebar":"someSidebar"},"common/file-storage/upload-and-download-files":{"id":"common/file-storage/upload-and-download-files","title":"Upload and Download Files","description":"Files can be uploaded and downloaded using FoalTS file system. It allows you to use different types of file storage such as the local file system or cloud storage.","sidebar":"someSidebar"},"common/graphql":{"id":"common/graphql","title":"GraphQL","description":"GraphQL is a query language for APIs. Unlike traditional REST APIs, GraphQL APIs have only one endpoint to which requests are sent. The content of the request describes all the operations to be performed and the data to be returned in the response. Many resources can be retrieved in a single request and the client gets exactly the properties it asks for.","sidebar":"someSidebar"},"common/gRPC":{"id":"common/gRPC","title":"gRPC","description":"gRPC is a Remote Procedure Call (RPC) framework that can run in any environment. It can connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.","sidebar":"someSidebar"},"common/logging":{"id":"common/logging","title":"Logging","description":"Foal provides an advanced built-in logger. This page shows how to use it.","sidebar":"someSidebar"},"common/openapi-and-swagger-ui":{"id":"common/openapi-and-swagger-ui","title":"OpenAPI & Swagger UI","description":"Introduction","sidebar":"someSidebar"},"common/rest-blueprints":{"id":"common/rest-blueprints","title":"REST API","description":"Example:","sidebar":"someSidebar"},"common/serialization":{"id":"common/serialization","title":"Serialization","description":"This document shows how to serialize class instances into plain objects and, conversely, how to deserialize plain objects into class instances. It is based on the class-transformer library.","sidebar":"someSidebar"},"common/utilities":{"id":"common/utilities","title":"Utilities","description":"Random Tokens","sidebar":"someSidebar"},"common/validation-and-sanitization":{"id":"common/validation-and-sanitization","title":"Validation & Sanitization","description":"Validation checks if an input meets a set of criteria (such as the value of a property is a string).","sidebar":"someSidebar"},"common/websockets":{"id":"common/websockets","title":"Real-Time Communication","description":"Foal allows you to establish two-way interactive communication between your server(s) and your clients. For this, it uses the socket.io v4 library which is primarily based on the WebSocket protocol. It supports disconnection detection and automatic reconnection and works with proxies and load balancers.","sidebar":"someSidebar"},"community/awesome-foal":{"id":"community/awesome-foal","title":"Awesome Foal","description":"A collection of official and unofficial libraries and resources about Foal.","sidebar":"someSidebar"},"comparison-with-other-frameworks/express-fastify":{"id":"comparison-with-other-frameworks/express-fastify","title":"Foal vs Express or Fastify","description":"These pages are definitely the most difficult to write. If you are here, it is probably because you want to know if you should choose Foal over another framework. There are many in the Node ecosystem and choosing one is not always an easy task. These pages are meant to help you on your way.","sidebar":"someSidebar"},"databases/other-orm/introduction":{"id":"databases/other-orm/introduction","title":"Using Another ORM","description":"The core of the framework is independent of TypeORM. So, if you do not want to use an ORM at all or use another ORM or ODM than TypeORM, you absolutely can.","sidebar":"someSidebar"},"databases/other-orm/prisma":{"id":"databases/other-orm/prisma","title":"Prisma","description":"This document shows how to configure Prisma in a FoalTS project. It assumes that you have already uninstalled TypeORM.","sidebar":"someSidebar"},"databases/typeorm/create-models-and-queries":{"id":"databases/typeorm/create-models-and-queries","title":"Models & Queries","description":"Entities","sidebar":"someSidebar"},"databases/typeorm/generate-and-run-migrations":{"id":"databases/typeorm/generate-and-run-migrations","title":"Generate & Run Migrations","description":"Database migrations are a way of propagating changes you make to your entities into your database schema. The changes you make to your models (adding a field, deleting an entity, etc.) do not automatically modify your database. You have to do it yourself.","sidebar":"someSidebar"},"databases/typeorm/introduction":{"id":"databases/typeorm/introduction","title":"TypeORM","description":"A simple model:","sidebar":"someSidebar"},"databases/typeorm/mongodb":{"id":"databases/typeorm/mongodb","title":"MongoDB (noSQL)","description":"Creating a new project","sidebar":"someSidebar"},"deployment-and-environments/checklist":{"id":"deployment-and-environments/checklist","title":"Deployment Checklist","description":"Set the Node.JS environment to production","sidebar":"someSidebar"},"frontend/angular-react-vue":{"id":"frontend/angular-react-vue","title":"Angular, React & Vue","description":"Angular, React and Vue all provide powerful CLIs for creating frontend applications. These tools are widely used, regularly improved and extensively documented. That\'s why Foal CLI do not provide ready-made features to build the frontend in their place.","sidebar":"someSidebar"},"frontend/not-found-page":{"id":"frontend/not-found-page","title":"404 Page","description":"Here\'s a way to implement custom 404 pages.","sidebar":"someSidebar"},"frontend/nuxt.js":{"id":"frontend/nuxt.js","title":"Nuxt","description":"Nuxt is a frontend framework based on Vue.JS.","sidebar":"someSidebar"},"frontend/server-side-rendering":{"id":"frontend/server-side-rendering","title":"Server-Side Rendering","description":"Regular Templates","sidebar":"someSidebar"},"frontend/single-page-applications":{"id":"frontend/single-page-applications","title":"Single Page Applications (SPA)","description":"Single-Page Applications are Web Applications that are loaded once upon the first request(s) to the backend. After retreiving all the necessary code from the server, the current page is rendered and updated directly in the browser without asking the server to render new pages. During its lifecycle, the application usually communicates with the server by making API calls to fetch, create, update or delete data. This is a common pattern used when creating a new application with Angular, React or Vue.","sidebar":"someSidebar"},"README":{"id":"README","title":"Introduction","description":"License: MIT","sidebar":"someSidebar"},"security/body-size-limiting":{"id":"security/body-size-limiting","title":"Body Size Limiting","description":"By default, FoalTS only accepts request bodies lower than 100kb. This value can be increased by using the configuration key settings.bodyParser.limit. If a number is provided, then the value specifies the number of bytes. If it is a string, the value is passed to the bytes library for parsing.","sidebar":"someSidebar"},"security/cors":{"id":"security/cors","title":"CORS Requests","description":"Building a public API requires to allow Cross-Origin Request Sharing.","sidebar":"someSidebar"},"security/csrf-protection":{"id":"security/csrf-protection","title":"CSRF Protection","description":"Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious web site, email, blog, instant message, or program causes a user\u2019s web browser to perform an unwanted action on a trusted site when the user is authenticated.","sidebar":"someSidebar"},"security/http-headers-protection":{"id":"security/http-headers-protection","title":"Protection HTTP Headers","description":"To protect the application against some common attacks, FoalTS sets by default various HTTP headers. These can be overrided in the HttpResponse objects.","sidebar":"someSidebar"},"security/rate-limiting":{"id":"security/rate-limiting","title":"Rate Limiting","description":"To avoid brute force attacks or overloading your application, you can set up a rate limiter to limit the number of requests a user can send to your application.","sidebar":"someSidebar"},"testing/e2e-testing":{"id":"testing/e2e-testing","title":"E2E Testing","description":"End-to-end tests are located in the src/e2e directory.","sidebar":"someSidebar"},"testing/introduction":{"id":"testing/introduction","title":"Introduction","description":"Every shipped app should come with a minimum set of tests. Writing tests lets you find problems early, facilitate changes and document your code. FoalTS is designed to be easily testable and provides the tools you need to write tests right away.","sidebar":"someSidebar"},"testing/unit-testing":{"id":"testing/unit-testing","title":"Unit Testing","description":"Convention","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-1-introduction":{"id":"tutorials/real-world-example-with-react/tuto-1-introduction","title":"Introduction","description":"This tutorial shows how to build a real-world application with React and Foal. It assumes that you have already read the first guide How to build a Simple To-Do List and that you have a basic knowledge of React.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-10-auth-with-react":{"id":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","title":"Authenticating Users in React","description":"The backend API is ready to be used. Now let\'s add authentication in the frontend side.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-11-sign-up":{"id":"tutorials/real-world-example-with-react/tuto-11-sign-up","title":"Adding Sign Up","description":"So far, only users created with the create-user script can log in and publish stories. In this section you will add a new API endpoint for users to sign up with the registration page.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-12-file-upload":{"id":"tutorials/real-world-example-with-react/tuto-12-file-upload","title":"Image Upload and Download","description":"The next step in this tutorial is to allow users to upload a profile picture. This image will be displayed on the homepage in front of each author\'s story.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-13-csrf":{"id":"tutorials/real-world-example-with-react/tuto-13-csrf","title":"CSRF Protection","description":"Since you use authentication with cookies, you need to add CSRF protection to your application. This is really easy with Foal, even when building a SPA.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-14-production-build":{"id":"tutorials/real-world-example-with-react/tuto-14-production-build","title":"Production Build","description":"So far, the front-end and back-end applications are compiled and served by two different development servers. The next step is to build them into a single one ready for production.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-15-social-auth":{"id":"tutorials/real-world-example-with-react/tuto-15-social-auth","title":"Social Auth with Google","description":"In this last part of the tutorial, we will give users the ability to log in with Google. Currently, they can only log in with an email and a password.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-2-database-set-up":{"id":"tutorials/real-world-example-with-react/tuto-2-database-set-up","title":"Database Set Up","description":"The first step in this tutorial is to establish a database connection. If you haven\'t already done so, install MySQL or PostgreSQL.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-3-the-models":{"id":"tutorials/real-world-example-with-react/tuto-3-the-models","title":"The User and Story Models","description":"Now that the database connection is established, you can create your two entities User and Story.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts":{"id":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","title":"The Shell Scripts","description":"Your models are ready to be used. As in the previous tutorial, you will use shell scripts to feed the database.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-5-our-first-route":{"id":"tutorials/real-world-example-with-react/tuto-5-our-first-route","title":"Your First Route","description":"The database is now filled with some stories. Let\'s implement the first route to retrieve them.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-6-swagger-interface":{"id":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","title":"API Testing with Swagger","description":"Now that the first API endpoint has been implemented, the next step is to test it.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-7-add-frontend":{"id":"tutorials/real-world-example-with-react/tuto-7-add-frontend","title":"The Frontend App","description":"Very good, so far you have a first working version of your API. It\'s time to add the frontend.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-8-authentication":{"id":"tutorials/real-world-example-with-react/tuto-8-authentication","title":"Logging Users In and Out","description":"Stories are displayed on the home page. If we want users to be able to post new stories and upload a profile picture, we need to allow them to log in to the application.","sidebar":"someSidebar"},"tutorials/real-world-example-with-react/tuto-9-authenticated-api":{"id":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","title":"Authenticating Users in the API","description":"Now that the login is configured, you can add two new routes to create and delete stories. Their access will be limited to authenticated users.","sidebar":"someSidebar"},"tutorials/simple-todo-list/installation-troubleshooting":{"id":"tutorials/simple-todo-list/installation-troubleshooting","title":"Installation Troubleshooting","description":"Errors with node-gyp"},"tutorials/simple-todo-list/tuto-1-installation":{"id":"tutorials/simple-todo-list/tuto-1-installation","title":"Installation","description":"In this tutorial you will learn how to create a basic web application with FoalTS. The demo application is a simple to-do list with which users can view, create and delete their tasks.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-2-introduction":{"id":"tutorials/simple-todo-list/tuto-2-introduction","title":"Introduction","description":"The application that you will create is a simple to-do list. It consists of a frontend part that has already been written for you and a backend part that will be the topic of this tutorial.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-3-the-todo-model":{"id":"tutorials/simple-todo-list/tuto-3-the-todo-model","title":"The Todo Model","description":"The next step is to take care of the database. By default, every new project in FoalTS is configured to use an SQLite database as it does not require any additional installation.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo":{"id":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","title":"The Shell Script create-todo","description":"Now it is time to populate the database with some tasks.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-5-the-rest-api":{"id":"tutorials/simple-todo-list/tuto-5-the-rest-api","title":"The REST API","description":"Good, so far you have a frontend working properly and some todos in your database. Now it is time to code a REST API to link them both.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-6-validation-and-sanitization":{"id":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","title":"Validation & Sanitization","description":"Currently inputs received by the server are not checked. Everyone could send anything when requesting POST /api/todos. That\'s why client inputs cannot be trusted.","sidebar":"someSidebar"},"tutorials/simple-todo-list/tuto-7-unit-testing":{"id":"tutorials/simple-todo-list/tuto-7-unit-testing","title":"Unit Testing","description":"The last step of this tutorial is to add some unit tests to the ApiController.","sidebar":"someSidebar"}}}')}}]); \ No newline at end of file diff --git a/assets/js/9c021584.d84dd1ea.js b/assets/js/9c021584.5d8f156f.js similarity index 77% rename from assets/js/9c021584.d84dd1ea.js rename to assets/js/9c021584.5d8f156f.js index 9d0e1a7709..0e37006787 100644 --- a/assets/js/9c021584.d84dd1ea.js +++ b/assets/js/9c021584.5d8f156f.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1307],{5173:e=>{e.exports=JSON.parse('{"permalink":"/blog/tags/release","page":1,"postsPerPage":10,"totalPages":3,"totalCount":24,"nextPage":"/blog/tags/release/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1307],{5173:e=>{e.exports=JSON.parse('{"permalink":"/blog/tags/release","page":1,"postsPerPage":10,"totalPages":3,"totalCount":25,"nextPage":"/blog/tags/release/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/assets/js/9ca94865.61a7a8bb.js b/assets/js/9ca94865.61a7a8bb.js deleted file mode 100644 index ade0c815f2..0000000000 --- a/assets/js/9ca94865.61a7a8bb.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2674],{26629:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var o=n(74848),i=n(28453);const r={title:"The Todo Model",id:"tuto-3-the-todo-model",slug:"3-the-todo-model"},s=void 0,a={id:"tutorials/simple-todo-list/tuto-3-the-todo-model",title:"The Todo Model",description:"The next step is to take care of the database. By default, every new project in FoalTS is configured to use an SQLite database as it does not require any additional installation.",source:"@site/docs/tutorials/simple-todo-list/3-the-todo-model.md",sourceDirName:"tutorials/simple-todo-list",slug:"/tutorials/simple-todo-list/3-the-todo-model",permalink:"/docs/tutorials/simple-todo-list/3-the-todo-model",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/simple-todo-list/3-the-todo-model.md",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"The Todo Model",id:"tuto-3-the-todo-model",slug:"3-the-todo-model"},sidebar:"someSidebar",previous:{title:"Introduction",permalink:"/docs/tutorials/simple-todo-list/2-introduction"},next:{title:"The Shell Script create-todo",permalink:"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo"}},d={},l=[];function c(e){const t={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",p:"p",pre:"pre",...(0,i.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(t.p,{children:["The next step is to take care of the database. By default, every new project in FoalTS is configured to use an ",(0,o.jsx)(t.code,{children:"SQLite"})," database as it does not require any additional installation."]}),"\n",(0,o.jsx)(t.p,{children:"Let\u2019s start by creating your first model. The CLI provides a useful command to generate a new file with an empty model."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-sh",children:"foal generate entity todo\n"})}),"\n",(0,o.jsx)(t.admonition,{type:"info",children:(0,o.jsxs)(t.p,{children:["FoalTS uses ",(0,o.jsx)(t.a,{href:"http://typeorm.io",children:"TypeORM"})," as the default ORM in any new application. This way, you don't have to configure anything and you can start a project quickly. However, if you wish, you still can choose to ",(0,o.jsx)(t.a,{href:"/docs/databases/other-orm/introduction",children:"use another one"})," (",(0,o.jsx)(t.a,{href:"https://www.prisma.io/",children:"Prisma"}),", ",(0,o.jsx)(t.a,{href:"https://mikro-orm.io/",children:"MikroORM"}),", ",(0,o.jsx)(t.a,{href:"https://mongoosejs.com/",children:"Mongoose"}),", etc), as the framework code is ORM independent."]})}),"\n",(0,o.jsxs)(t.p,{children:["Open the file ",(0,o.jsx)(t.code,{children:"todo.entity.ts"})," in the ",(0,o.jsx)(t.code,{children:"src/app/entities"})," directory and add a ",(0,o.jsx)(t.code,{children:"text"})," column."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Todo extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n text: string;\n\n}\n\n"})}),"\n",(0,o.jsxs)(t.p,{children:["This class is the representation of the SQL table ",(0,o.jsx)(t.code,{children:"todo"}),". Currently, this table does not exist in the database. You will have to create it."]}),"\n",(0,o.jsx)(t.p,{children:"You can do this manually, using a database software for example, or use migrations, a programmatic way to update a database schema. The advantage of using migrations is that you can create, update and delete your tables directly from the definition of your entities. They also ensure that all your environments (prod, dev) and co-developers have the same table definitions."}),"\n",(0,o.jsx)(t.p,{children:"Let\u2019s see how to use them."}),"\n",(0,o.jsx)(t.p,{children:"First run the following command to generate your migration file. TypeORM will compare your current database schema with the definition of your entities and generate the appropriate SQL queries."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"npm run makemigrations\n"})}),"\n",(0,o.jsxs)(t.p,{children:["A new file appears in the ",(0,o.jsx)(t.code,{children:"src/migrations/"})," directory. Open it."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:'import {MigrationInterface, QueryRunner} from "typeorm";\n\nexport class migration1562755564200 implements MigrationInterface {\n\n public async up(queryRunner: QueryRunner): Promise {\n await queryRunner.query(`CREATE TABLE "todo" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "text" varchar NOT NULL)`, undefined);\n await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL)`, undefined);\n }\n\n public async down(queryRunner: QueryRunner): Promise {\n await queryRunner.query(`DROP TABLE "user"`, undefined);\n await queryRunner.query(`DROP TABLE "todo"`, undefined);\n }\n\n}\n\n'})}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"up"})," method contains all the SQL queries that will be executed during the migration."]}),"\n",(0,o.jsx)(t.p,{children:"Then run the migration."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"npm run migrations\n"})}),"\n",(0,o.jsx)(t.p,{children:"TypeORM examines all the migrations that have been run previously (none in this case) and executes the new ones."}),"\n",(0,o.jsxs)(t.p,{children:["Your database (",(0,o.jsx)(t.code,{children:"db.sqlite3"}),") now contains a new table named ",(0,o.jsx)(t.code,{children:"todo"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"+------------+-----------+-------------------------------------+\n| todo |\n+------------+-----------+-------------------------------------+\n| id | integer | PRIMARY KEY AUTO_INCREMENT NOT NULL |\n| text | varchar | NOT NULL |\n+------------+-----------+-------------------------------------+\n"})}),"\n",(0,o.jsxs)(t.blockquote,{children:["\n",(0,o.jsxs)(t.p,{children:["Alternatively, you can also use the ",(0,o.jsx)(t.code,{children:"database.synchronize"})," option in your configuration file ",(0,o.jsx)(t.code,{children:"config/default.json"}),". When set to ",(0,o.jsx)(t.code,{children:"true"}),", the database schema is auto created from the entities definition on every application launch. You do not need migrations in this case. However, although this behavior may be useful during debug and development, it is not suitable for a production environment (you could lose production data)."]}),"\n"]})]})}function u(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>a});var o=n(96540);const i={},r=o.createContext(i);function s(e){const t=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/9ca94865.dad0ea29.js b/assets/js/9ca94865.dad0ea29.js new file mode 100644 index 0000000000..92d0aaf38b --- /dev/null +++ b/assets/js/9ca94865.dad0ea29.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2674],{26629:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var o=n(74848),i=n(28453);const r={title:"The Todo Model",id:"tuto-3-the-todo-model",slug:"3-the-todo-model"},s=void 0,a={id:"tutorials/simple-todo-list/tuto-3-the-todo-model",title:"The Todo Model",description:"The next step is to take care of the database. By default, every new project in FoalTS is configured to use an SQLite database as it does not require any additional installation.",source:"@site/docs/tutorials/simple-todo-list/3-the-todo-model.md",sourceDirName:"tutorials/simple-todo-list",slug:"/tutorials/simple-todo-list/3-the-todo-model",permalink:"/docs/tutorials/simple-todo-list/3-the-todo-model",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/simple-todo-list/3-the-todo-model.md",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"The Todo Model",id:"tuto-3-the-todo-model",slug:"3-the-todo-model"},sidebar:"someSidebar",previous:{title:"Introduction",permalink:"/docs/tutorials/simple-todo-list/2-introduction"},next:{title:"The Shell Script create-todo",permalink:"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo"}},d={},l=[];function c(e){const t={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",p:"p",pre:"pre",...(0,i.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsxs)(t.p,{children:["The next step is to take care of the database. By default, every new project in FoalTS is configured to use an ",(0,o.jsx)(t.code,{children:"SQLite"})," database as it does not require any additional installation."]}),"\n",(0,o.jsx)(t.p,{children:"Let\u2019s start by creating your first model. The CLI provides a useful command to generate a new file with an empty model."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-sh",children:"npx foal generate entity todo\n"})}),"\n",(0,o.jsx)(t.admonition,{type:"info",children:(0,o.jsxs)(t.p,{children:["FoalTS uses ",(0,o.jsx)(t.a,{href:"http://typeorm.io",children:"TypeORM"})," as the default ORM in any new application. This way, you don't have to configure anything and you can start a project quickly. However, if you wish, you still can choose to ",(0,o.jsx)(t.a,{href:"/docs/databases/other-orm/introduction",children:"use another one"})," (",(0,o.jsx)(t.a,{href:"https://www.prisma.io/",children:"Prisma"}),", ",(0,o.jsx)(t.a,{href:"https://mikro-orm.io/",children:"MikroORM"}),", ",(0,o.jsx)(t.a,{href:"https://mongoosejs.com/",children:"Mongoose"}),", etc), as the framework code is ORM independent."]})}),"\n",(0,o.jsxs)(t.p,{children:["Open the file ",(0,o.jsx)(t.code,{children:"todo.entity.ts"})," in the ",(0,o.jsx)(t.code,{children:"src/app/entities"})," directory and add a ",(0,o.jsx)(t.code,{children:"text"})," column."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Todo extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n text: string;\n\n}\n\n"})}),"\n",(0,o.jsxs)(t.p,{children:["This class is the representation of the SQL table ",(0,o.jsx)(t.code,{children:"todo"}),". Currently, this table does not exist in the database. You will have to create it."]}),"\n",(0,o.jsx)(t.p,{children:"You can do this manually, using a database software for example, or use migrations, a programmatic way to update a database schema. The advantage of using migrations is that you can create, update and delete your tables directly from the definition of your entities. They also ensure that all your environments (prod, dev) and co-developers have the same table definitions."}),"\n",(0,o.jsx)(t.p,{children:"Let\u2019s see how to use them."}),"\n",(0,o.jsx)(t.p,{children:"First run the following command to generate your migration file. TypeORM will compare your current database schema with the definition of your entities and generate the appropriate SQL queries."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"npm run makemigrations\n"})}),"\n",(0,o.jsxs)(t.p,{children:["A new file appears in the ",(0,o.jsx)(t.code,{children:"src/migrations/"})," directory. Open it."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:'import {MigrationInterface, QueryRunner} from "typeorm";\n\nexport class migration1562755564200 implements MigrationInterface {\n\n public async up(queryRunner: QueryRunner): Promise {\n await queryRunner.query(`CREATE TABLE "todo" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "text" varchar NOT NULL)`, undefined);\n await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL)`, undefined);\n }\n\n public async down(queryRunner: QueryRunner): Promise {\n await queryRunner.query(`DROP TABLE "user"`, undefined);\n await queryRunner.query(`DROP TABLE "todo"`, undefined);\n }\n\n}\n\n'})}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"up"})," method contains all the SQL queries that will be executed during the migration."]}),"\n",(0,o.jsx)(t.p,{children:"Then run the migration."}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"npm run migrations\n"})}),"\n",(0,o.jsx)(t.p,{children:"TypeORM examines all the migrations that have been run previously (none in this case) and executes the new ones."}),"\n",(0,o.jsxs)(t.p,{children:["Your database (",(0,o.jsx)(t.code,{children:"db.sqlite3"}),") now contains a new table named ",(0,o.jsx)(t.code,{children:"todo"}),":"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{children:"+------------+-----------+-------------------------------------+\n| todo |\n+------------+-----------+-------------------------------------+\n| id | integer | PRIMARY KEY AUTO_INCREMENT NOT NULL |\n| text | varchar | NOT NULL |\n+------------+-----------+-------------------------------------+\n"})}),"\n",(0,o.jsxs)(t.blockquote,{children:["\n",(0,o.jsxs)(t.p,{children:["Alternatively, you can also use the ",(0,o.jsx)(t.code,{children:"database.synchronize"})," option in your configuration file ",(0,o.jsx)(t.code,{children:"config/default.json"}),". When set to ",(0,o.jsx)(t.code,{children:"true"}),", the database schema is auto created from the entities definition on every application launch. You do not need migrations in this case. However, although this behavior may be useful during debug and development, it is not suitable for a production environment (you could lose production data)."]}),"\n"]})]})}function u(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>s,x:()=>a});var o=n(96540);const i={},r=o.createContext(i);function s(e){const t=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a252a406.5c88d19c.js b/assets/js/a252a406.5c88d19c.js deleted file mode 100644 index 31e991ce49..0000000000 --- a/assets/js/a252a406.5c88d19c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3766],{83788:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>h,frontMatter:()=>r,metadata:()=>i,toc:()=>a});var s=t(74848),o=t(28453);const r={title:"Real-Time Communication",sidebar_label:"WebSockets"},c=void 0,i={id:"common/websockets",title:"Real-Time Communication",description:"Foal allows you to establish two-way interactive communication between your server(s) and your clients. For this, it uses the socket.io v4 library which is primarily based on the WebSocket protocol. It supports disconnection detection and automatic reconnection and works with proxies and load balancers.",source:"@site/docs/common/websockets.md",sourceDirName:"common",slug:"/common/websockets",permalink:"/docs/common/websockets",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/websockets.md",tags:[],version:"current",frontMatter:{title:"Real-Time Communication",sidebar_label:"WebSockets"},sidebar:"someSidebar",previous:{title:"GraphQL",permalink:"/docs/common/graphql"},next:{title:"gRPC",permalink:"/docs/common/gRPC"}},l={},a=[{value:"Get Started",id:"get-started",level:2},{value:"Server",id:"server",level:3},{value:"Client",id:"client",level:3},{value:"Architecture",id:"architecture",level:2},{value:"Controllers and hooks",id:"controllers-and-hooks",level:3},{value:"Contexts",id:"contexts",level:4},{value:"Responses",id:"responses",level:4},{value:"Hooks",id:"hooks",level:4},{value:"Summary table",id:"summary-table",level:4},{value:"Send a message",id:"send-a-message",level:3},{value:"Broadcast a message",id:"broadcast-a-message",level:3},{value:"Grouping clients in rooms",id:"grouping-clients-in-rooms",level:3},{value:"Accessing the socket.io server",id:"accessing-the-socketio-server",level:3},{value:"Error-handling",id:"error-handling",level:3},{value:"Customizing the error handler",id:"customizing-the-error-handler",level:4},{value:"Payload Validation",id:"payload-validation",level:2},{value:"Unit Testing",id:"unit-testing",level:2},{value:"Advanced",id:"advanced",level:2},{value:"Multiple node servers",id:"multiple-node-servers",level:3},{value:"Handling connection",id:"handling-connection",level:3},{value:"Error-handling",id:"error-handling-1",level:4},{value:"Custom server options",id:"custom-server-options",level:3}];function d(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["Foal allows you to establish two-way interactive communication between your server(s) and your clients. For this, it uses the ",(0,s.jsx)(n.a,{href:"https://socket.io/",children:"socket.io v4"})," library which is primarily based on the ",(0,s.jsx)(n.strong,{children:"WebSocket"})," protocol. It supports disconnection detection and automatic reconnection and works with proxies and load balancers."]}),"\n",(0,s.jsx)(n.h2,{id:"get-started",children:"Get Started"}),"\n",(0,s.jsx)(n.h3,{id:"server",children:"Server"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install @foal/socket.io\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"controllers/websocket.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, ValidatePayload, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n @EventName('create product')\n @ValidatePayload({\n additionalProperties: false,\n properties: { name: { type: 'string' }},\n required: [ 'name' ],\n type: 'object'\n })\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\n const product = new Product();\n product.name = payload.name;\n await product.save();\n\n // Send a message to all clients.\n ctx.socket.broadcast.emit('refresh products');\n return new WebsocketResponse();\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"src/index.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// ...\n\nimport * as http from 'http';\n\nasync function main() {\n const serviceManager = new ServiceManager();\n\n const app = await createApp(AppController, { serviceManager });\n\n const httpServer = http.createServer(app);\n // Instanciate, init and connect websocket controllers.\n await serviceManager.get(WebsocketController).attachHttpServer(httpServer);\n httpServer.listen(port, () => displayServerURL(port));\n}\n\n"})}),"\n",(0,s.jsx)(n.h3,{id:"client",children:"Client"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["This example uses JavaScript code as client, but socket.io supports also ",(0,s.jsx)(n.a,{href:"https://socket.io/docs/v4",children:"many other languages"})," (python, java, etc)."]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install socket.io-client@4\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { io } from 'socket.io-client';\n\nconst socket = io('ws://localhost:3001');\n\nsocket.on('connect', () => {\n\n socket.emit('create product', { name: 'product 1' }, response => {\n if (response.status === 'error') {\n console.log(response.error);\n }\n });\n\n});\n\nsocket.on('connect_error', () => {\n console.log('Impossible to establish the socket.io connection');\n});\n\nsocket.on('refresh products', () => {\n console.log('refresh products!');\n});\n"})}),"\n",(0,s.jsxs)(n.admonition,{type:"info",children:[(0,s.jsxs)(n.p,{children:["When using socket.io with FoalTS, the client function ",(0,s.jsx)(n.code,{children:"emit"})," can only take one, two or three arguments."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"socket.emit('event name');\nsocket.emit('event name', { /* payload */ });\nsocket.emit('event name', { /* payload */ }, response => { /* do something */ });\n"})})]}),"\n",(0,s.jsx)(n.h2,{id:"architecture",children:"Architecture"}),"\n",(0,s.jsx)(n.h3,{id:"controllers-and-hooks",children:"Controllers and hooks"}),"\n",(0,s.jsx)(n.p,{children:"The WebSocket architecture is very similar to the HTTP architecture. They both have controllers and hooks. While HTTP controllers use paths to handle the various application endpoints, websocket controllers use event names. As with HTTP, event names can be extended with subcontrollers."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"user.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n createUser(ctx: WebsocketContext) {\n // ...\n }\n\n @EventName('delete')\n deleteUser(ctx: WebsocketContext) {\n // ...\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"websocket.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { SocketIOController, wsController } from '@foal/socket.io';\n\nimport { UserController } from './user.controller.ts';\n\nexport class WebsocketController extends SocketIOController {\n subControllers = [\n wsController('users ', UserController)\n ];\n}\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Note that the event names are simply concatenated. So you have to manage the spaces between the words yourself if there are any."}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"contexts",children:"Contexts"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"Context"})," and ",(0,s.jsx)(n.code,{children:"WebsocketContext"})," classes share common properties such as the ",(0,s.jsx)(n.code,{children:"state"}),", the ",(0,s.jsx)(n.code,{children:"user"})," and the ",(0,s.jsx)(n.code,{children:"session"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["However, unlike their HTTP version, instances of ",(0,s.jsx)(n.code,{children:"WebsocketContext"})," do not have a ",(0,s.jsx)(n.code,{children:"request"})," property but a ",(0,s.jsx)(n.code,{children:"socket"})," property which is the object provided by socket.io. They also have three other attributes: the ",(0,s.jsx)(n.code,{children:"eventName"}),", the ",(0,s.jsx)(n.code,{children:"payload"})," of the request as well as a ",(0,s.jsx)(n.code,{children:"messageId"}),"."]}),"\n",(0,s.jsx)(n.h4,{id:"responses",children:"Responses"}),"\n",(0,s.jsxs)(n.p,{children:["A controller method returns a response which is either a ",(0,s.jsx)(n.code,{children:"WebsocketResponse"})," or a ",(0,s.jsx)(n.code,{children:"WebsocketErrorResponse"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["If a ",(0,s.jsx)(n.code,{children:"WebsocketResponse(data)"})," is returned, the server will return to the client an object of this form:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"{\n status: 'ok',\n data: data\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If it is a ",(0,s.jsx)(n.code,{children:"WebsocketErrorResponse(error)"}),", the returned object will look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"{\n status: 'error',\n error: error\n}\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["Note that the ",(0,s.jsx)(n.code,{children:"data"})," and ",(0,s.jsx)(n.code,{children:"error"})," parameters are both optional."]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"hooks",children:"Hooks"}),"\n",(0,s.jsxs)(n.p,{children:["In the same way, Foal provides hooks for websockets. They work the same as their HTTP version except that some types are different (",(0,s.jsx)(n.code,{children:"WebsocketContext"}),", ",(0,s.jsx)(n.code,{children:"WebsocketResponse|WebsocketErrorResponse"}),")."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext, WebsocketErrorResponse, WebsocketHook } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n @WebsocketHook((ctx, services) => {\n if (typeof ctx.payload.name !== 'string') {\n return new WebsocketErrorResponse('Invalid name type');\n }\n })\n createUser(ctx: WebsocketContext) {\n // ...\n }\n}\n"})}),"\n",(0,s.jsx)(n.h4,{id:"summary-table",children:"Summary table"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"HTTP"}),(0,s.jsx)(n.th,{children:"Websocket"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"@Get"}),", ",(0,s.jsx)(n.code,{children:"@Post"}),", etc"]}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"@EventName"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"controller"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"wsController"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Context"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WebsocketContext"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"HttpResponse"}),"(s)"]}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"WebsocketResponse"}),", ",(0,s.jsx)(n.code,{children:"WebsocketErrorResponse"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Hook"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WebsocketHook"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MergeHooks"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MergeWebsocketHooks"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"getHookFunction"}),", ",(0,s.jsx)(n.code,{children:"getHookFunctions"})]}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"getWebsocketHookFunction"}),", ",(0,s.jsx)(n.code,{children:"getWebsocketHookFunctions"})]})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"send-a-message",children:"Send a message"}),"\n",(0,s.jsxs)(n.p,{children:["At any time, the server can send one or more messages to the client using its ",(0,s.jsx)(n.code,{children:"socket"})," object."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Server code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n createUser(ctx: WebsocketContext) {\n ctx.socket.emit('event 1', 'first message');\n ctx.socket.emit('event 1', 'second message');\n return new WebsocketResponse();\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Client code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"socket.on('event 1', payload => {\n console.log('Message: ', payload);\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"broadcast-a-message",children:"Broadcast a message"}),"\n",(0,s.jsxs)(n.p,{children:["If a message is to be broadcast to all clients, you can use the ",(0,s.jsx)(n.code,{children:"broadcast"})," property for this."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Server code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n createUser(ctx: WebsocketContext) {\n ctx.socket.broadcast.emit('event 1', 'first message');\n ctx.socket.broadcast.emit('event 1', 'second message');\n return new WebsocketResponse();\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Client code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"socket.on('event 1', payload => {\n console.log('Message: ', payload);\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"grouping-clients-in-rooms",children:"Grouping clients in rooms"}),"\n",(0,s.jsxs)(n.p,{children:["Socket.io uses the concept of ",(0,s.jsx)(n.a,{href:"https://socket.io/docs/v4/rooms/",children:"rooms"})," to gather clients in groups. This can be useful if you need to send a message to a particular subset of clients."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n onConnection(ctx: WebsocketContext) {\n ctx.socket.join('some room');\n }\n\n @EventName('event 1')\n createUser(ctx: WebsocketContext) {\n ctx.socket.to('some room').emit('event 2');\n return new WebsocketResponse();\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"accessing-the-socketio-server",children:"Accessing the socket.io server"}),"\n",(0,s.jsxs)(n.p,{children:["You can access the socket.io server anywhere in your code (including your HTTP controllers) by injecting the ",(0,s.jsx)(n.code,{children:"WsServer"})," service."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { dependency, HttpResponseOK, Post } from '@foal/core';\nimport { WsServer } from '@foal/socket.io';\n\nexport class UserController {\n @dependency\n wsServer: WsServer;\n\n @Post('/users')\n createUser() {\n // ...\n this.wsServer.io.emit('refresh users');\n\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"error-handling",children:"Error-handling"}),"\n",(0,s.jsxs)(n.p,{children:["Any error thrown or rejected in a websocket controller, hook or service, if not caught, is converted to a ",(0,s.jsx)(n.code,{children:"WebsocketResponseError"}),". If the ",(0,s.jsx)(n.code,{children:"settings.debug"})," configuration parameter is ",(0,s.jsx)(n.code,{children:"true"}),", then the error is returned as is to the client. Otherwise, the server returns this response:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"({\n status: 'error',\n error: {\n code: 'INTERNAL_SERVER_ERROR',\n message: 'An internal server error has occurred.'\n }\n})\n"})}),"\n",(0,s.jsx)(n.h4,{id:"customizing-the-error-handler",children:"Customizing the error handler"}),"\n",(0,s.jsxs)(n.p,{children:["Just as its HTTP version, the ",(0,s.jsx)(n.code,{children:"SocketIOController"})," class supports an optional ",(0,s.jsx)(n.code,{children:"handleError"})," to override the default error handler."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, renderWebsocketError, SocketIOController, WebsocketContext, WebsocketErrorResponse } from '@foal/socket.io';\n\nclass PermissionDenied extends Error {}\n\nexport class WebsocketController extends SocketIOController implements ISocketIOController {\n @EventName('create user')\n createUser() {\n throw new PermissionDenied();\n }\n\n handleError(error: Error, ctx: WebsocketContext){\n if (error instanceof PermissionDenied) {\n return new WebsocketErrorResponse('Permission is denied');\n }\n\n return renderWebsocketError(error, ctx);\n }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"payload-validation",children:"Payload Validation"}),"\n",(0,s.jsxs)(n.p,{children:["Foal provides a default hook ",(0,s.jsx)(n.code,{children:"@ValidatePayload"})," to validate the request payload. It is very similar to its HTTP version ",(0,s.jsx)(n.code,{children:"@ValidateBody"}),"."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Server code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n @EventName('create product')\n @ValidatePayload({\n additionalProperties: false,\n properties: { name: { type: 'string' }},\n required: [ 'name' ],\n type: 'object'\n })\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\n const product = new Product();\n product.name = payload.name;\n await product.save();\n\n // Send a message to all clients.\n ctx.socket.broadcast.emit('refresh products');\n return new WebsocketResponse();\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Validation error response"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"({\n status: 'error',\n error: {\n code: 'VALIDATION_PAYLOAD_ERROR',\n payload: [\n // errors\n ]\n }\n})\n"})}),"\n",(0,s.jsx)(n.h2,{id:"unit-testing",children:"Unit Testing"}),"\n",(0,s.jsxs)(n.p,{children:["Testing WebSocket controllers and hooks is very similar to testing their HTTP equivalent. The ",(0,s.jsx)(n.code,{children:"WebsocketContext"})," takes three parameters."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Name"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"eventName"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"string"})}),(0,s.jsx)(n.td,{children:"The name of the event."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"payload"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"any"})}),(0,s.jsx)(n.td,{children:"The request payload."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"socket"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"any"})}),(0,s.jsxs)(n.td,{children:["The socket (optional). Default: ",(0,s.jsx)(n.code,{children:"{}"}),"."]})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,s.jsx)(n.h3,{id:"multiple-node-servers",children:"Multiple node servers"}),"\n",(0,s.jsx)(n.p,{children:"This example shows how to manage multiple node servers using a redis adapter."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install socket.io-adapter @socket.io/redis-adapter@8 redis@4\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"src/index.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { WebsocketController, pubClient, subClient } from './services/websocket.controller';\nasync function main() {\n const serviceManager = new ServiceManager();\n const app = await createApp(AppController, { serviceManager });\n const httpServer = http.createServer(app);\n // Connect the redis clients to the database.\n await Promise.all([pubClient.connect(), subClient.connect()]);\n await serviceManager.get(WebsocketController).attachHttpServer(httpServer);\n // ...\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"websocket.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\nimport { createAdapter } from '@socket.io/redis-adapter';\nimport { createClient } from 'redis';\n\nexport const pubClient = createClient({ url: 'redis://localhost:6379' });\nexport const subClient = pubClient.duplicate();\n\nexport class WebsocketController extends SocketIOController {\n adapter = createAdapter(pubClient, subClient);\n\n @EventName('create user')\n createUser(ctx: WebsocketContext) {\n // Broadcast an event to all clients of all servers.\n ctx.socket.broadcast.emit('refresh users');\n return new WebsocketResponse();\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"handling-connection",children:"Handling connection"}),"\n",(0,s.jsxs)(n.p,{children:["If you want to run some code when a Websocket connection is established (for example to join a room or forward the session), you can use the ",(0,s.jsx)(n.code,{children:"onConnection"})," method of the ",(0,s.jsx)(n.code,{children:"SocketIOController"})," for this."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { SocketIOController, WebsocketContext } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n onConnection(ctx: WebsocketContext) {\n // ...\n }\n\n}\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["The context passed in the ",(0,s.jsx)(n.code,{children:"onConnection"})," method has an undefined payload and an empty event name."]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"error-handling-1",children:"Error-handling"}),"\n",(0,s.jsxs)(n.p,{children:["Any errors thrown or rejected in the ",(0,s.jsx)(n.code,{children:"onConnection"})," is sent back to the client. So you may need to add a ",(0,s.jsx)(n.code,{children:"try {} catch {}"})," in some cases."]}),"\n",(0,s.jsxs)(n.p,{children:["This error can be read on the client using the ",(0,s.jsx)(n.code,{children:"connect_error"})," event listener."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:'socket.on("connect_error", () => {\n // Do some stuff\n socket.connect();\n});\n'})}),"\n",(0,s.jsx)(n.h3,{id:"custom-server-options",children:"Custom server options"}),"\n",(0,s.jsxs)(n.p,{children:["Custom options can be passed to the socket.io server as follows. The complete list of options can be found ",(0,s.jsx)(n.a,{href:"https://socket.io/docs/v4/server-options/",children:"here"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { SocketIOController } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n options = {\n connectTimeout: 60000\n }\n\n}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>c,x:()=>i});var s=t(96540);const o={},r=s.createContext(o);function c(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:c(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a252a406.b8f0ac26.js b/assets/js/a252a406.b8f0ac26.js new file mode 100644 index 0000000000..0043db55e2 --- /dev/null +++ b/assets/js/a252a406.b8f0ac26.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3766],{83788:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>h,frontMatter:()=>r,metadata:()=>i,toc:()=>a});var s=t(74848),o=t(28453);const r={title:"Real-Time Communication",sidebar_label:"WebSockets"},c=void 0,i={id:"common/websockets",title:"Real-Time Communication",description:"Foal allows you to establish two-way interactive communication between your server(s) and your clients. For this, it uses the socket.io v4 library which is primarily based on the WebSocket protocol. It supports disconnection detection and automatic reconnection and works with proxies and load balancers.",source:"@site/docs/common/websockets.md",sourceDirName:"common",slug:"/common/websockets",permalink:"/docs/common/websockets",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/websockets.md",tags:[],version:"current",frontMatter:{title:"Real-Time Communication",sidebar_label:"WebSockets"},sidebar:"someSidebar",previous:{title:"GraphQL",permalink:"/docs/common/graphql"},next:{title:"gRPC",permalink:"/docs/common/gRPC"}},l={},a=[{value:"Get Started",id:"get-started",level:2},{value:"Server",id:"server",level:3},{value:"Client",id:"client",level:3},{value:"Architecture",id:"architecture",level:2},{value:"Controllers and hooks",id:"controllers-and-hooks",level:3},{value:"Contexts",id:"contexts",level:4},{value:"Responses",id:"responses",level:4},{value:"Hooks",id:"hooks",level:4},{value:"Summary table",id:"summary-table",level:4},{value:"Send a message",id:"send-a-message",level:3},{value:"Broadcast a message",id:"broadcast-a-message",level:3},{value:"Grouping clients in rooms",id:"grouping-clients-in-rooms",level:3},{value:"Accessing the socket.io server",id:"accessing-the-socketio-server",level:3},{value:"Error-handling",id:"error-handling",level:3},{value:"Customizing the error handler",id:"customizing-the-error-handler",level:4},{value:"Payload Validation",id:"payload-validation",level:2},{value:"Unit Testing",id:"unit-testing",level:2},{value:"Advanced",id:"advanced",level:2},{value:"Multiple node servers",id:"multiple-node-servers",level:3},{value:"Handling connection",id:"handling-connection",level:3},{value:"Error-handling",id:"error-handling-1",level:4},{value:"Custom server options",id:"custom-server-options",level:3}];function d(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["Foal allows you to establish two-way interactive communication between your server(s) and your clients. For this, it uses the ",(0,s.jsx)(n.a,{href:"https://socket.io/",children:"socket.io v4"})," library which is primarily based on the ",(0,s.jsx)(n.strong,{children:"WebSocket"})," protocol. It supports disconnection detection and automatic reconnection and works with proxies and load balancers."]}),"\n",(0,s.jsx)(n.h2,{id:"get-started",children:"Get Started"}),"\n",(0,s.jsx)(n.h3,{id:"server",children:"Server"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install @foal/socket.io\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"controllers/websocket.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, ValidatePayload, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n @EventName('create product')\n @ValidatePayload({\n additionalProperties: false,\n properties: { name: { type: 'string' }},\n required: [ 'name' ],\n type: 'object'\n })\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\n const product = new Product();\n product.name = payload.name;\n await product.save();\n\n // Send a message to all clients.\n ctx.socket.broadcast.emit('refresh products');\n return new WebsocketResponse();\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"src/index.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// ...\n\nimport * as http from 'http';\n\nasync function main() {\n const serviceManager = new ServiceManager();\n const logger = serviceManager.get(Logger);\n\n const app = await createApp(AppController, { serviceManager });\n\n const httpServer = http.createServer(app);\n // Instanciate, init and connect websocket controllers.\n await serviceManager.get(WebsocketController).attachHttpServer(httpServer);\n httpServer.listen(port, () => logger.info(`Listening on port ${port}...`));\n}\n\n"})}),"\n",(0,s.jsx)(n.h3,{id:"client",children:"Client"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["This example uses JavaScript code as client, but socket.io supports also ",(0,s.jsx)(n.a,{href:"https://socket.io/docs/v4",children:"many other languages"})," (python, java, etc)."]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install socket.io-client@4\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { io } from 'socket.io-client';\n\nconst socket = io('ws://localhost:3001');\n\nsocket.on('connect', () => {\n\n socket.emit('create product', { name: 'product 1' }, response => {\n if (response.status === 'error') {\n console.log(response.error);\n }\n });\n\n});\n\nsocket.on('connect_error', () => {\n console.log('Impossible to establish the socket.io connection');\n});\n\nsocket.on('refresh products', () => {\n console.log('refresh products!');\n});\n"})}),"\n",(0,s.jsxs)(n.admonition,{type:"info",children:[(0,s.jsxs)(n.p,{children:["When using socket.io with FoalTS, the client function ",(0,s.jsx)(n.code,{children:"emit"})," can only take one, two or three arguments."]}),(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"socket.emit('event name');\nsocket.emit('event name', { /* payload */ });\nsocket.emit('event name', { /* payload */ }, response => { /* do something */ });\n"})})]}),"\n",(0,s.jsx)(n.h2,{id:"architecture",children:"Architecture"}),"\n",(0,s.jsx)(n.h3,{id:"controllers-and-hooks",children:"Controllers and hooks"}),"\n",(0,s.jsx)(n.p,{children:"The WebSocket architecture is very similar to the HTTP architecture. They both have controllers and hooks. While HTTP controllers use paths to handle the various application endpoints, websocket controllers use event names. As with HTTP, event names can be extended with subcontrollers."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"user.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n createUser(ctx: WebsocketContext) {\n // ...\n }\n\n @EventName('delete')\n deleteUser(ctx: WebsocketContext) {\n // ...\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"websocket.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { SocketIOController, wsController } from '@foal/socket.io';\n\nimport { UserController } from './user.controller.ts';\n\nexport class WebsocketController extends SocketIOController {\n subControllers = [\n wsController('users ', UserController)\n ];\n}\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Note that the event names are simply concatenated. So you have to manage the spaces between the words yourself if there are any."}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"contexts",children:"Contexts"}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"Context"})," and ",(0,s.jsx)(n.code,{children:"WebsocketContext"})," classes share common properties such as the ",(0,s.jsx)(n.code,{children:"state"}),", the ",(0,s.jsx)(n.code,{children:"user"})," and the ",(0,s.jsx)(n.code,{children:"session"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["However, unlike their HTTP version, instances of ",(0,s.jsx)(n.code,{children:"WebsocketContext"})," do not have a ",(0,s.jsx)(n.code,{children:"request"})," property but a ",(0,s.jsx)(n.code,{children:"socket"})," property which is the object provided by socket.io. They also have three other attributes: the ",(0,s.jsx)(n.code,{children:"eventName"}),", the ",(0,s.jsx)(n.code,{children:"payload"})," of the request as well as a ",(0,s.jsx)(n.code,{children:"messageId"}),"."]}),"\n",(0,s.jsx)(n.h4,{id:"responses",children:"Responses"}),"\n",(0,s.jsxs)(n.p,{children:["A controller method returns a response which is either a ",(0,s.jsx)(n.code,{children:"WebsocketResponse"})," or a ",(0,s.jsx)(n.code,{children:"WebsocketErrorResponse"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["If a ",(0,s.jsx)(n.code,{children:"WebsocketResponse(data)"})," is returned, the server will return to the client an object of this form:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"{\n status: 'ok',\n data: data\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If it is a ",(0,s.jsx)(n.code,{children:"WebsocketErrorResponse(error)"}),", the returned object will look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"{\n status: 'error',\n error: error\n}\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["Note that the ",(0,s.jsx)(n.code,{children:"data"})," and ",(0,s.jsx)(n.code,{children:"error"})," parameters are both optional."]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"hooks",children:"Hooks"}),"\n",(0,s.jsxs)(n.p,{children:["In the same way, Foal provides hooks for websockets. They work the same as their HTTP version except that some types are different (",(0,s.jsx)(n.code,{children:"WebsocketContext"}),", ",(0,s.jsx)(n.code,{children:"WebsocketResponse|WebsocketErrorResponse"}),")."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext, WebsocketErrorResponse, WebsocketHook } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n @WebsocketHook((ctx, services) => {\n if (typeof ctx.payload.name !== 'string') {\n return new WebsocketErrorResponse('Invalid name type');\n }\n })\n createUser(ctx: WebsocketContext) {\n // ...\n }\n}\n"})}),"\n",(0,s.jsx)(n.h4,{id:"summary-table",children:"Summary table"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"HTTP"}),(0,s.jsx)(n.th,{children:"Websocket"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"@Get"}),", ",(0,s.jsx)(n.code,{children:"@Post"}),", etc"]}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"@EventName"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"controller"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"wsController"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Context"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WebsocketContext"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"HttpResponse"}),"(s)"]}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"WebsocketResponse"}),", ",(0,s.jsx)(n.code,{children:"WebsocketErrorResponse"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Hook"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WebsocketHook"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MergeHooks"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MergeWebsocketHooks"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"getHookFunction"}),", ",(0,s.jsx)(n.code,{children:"getHookFunctions"})]}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"getWebsocketHookFunction"}),", ",(0,s.jsx)(n.code,{children:"getWebsocketHookFunctions"})]})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"send-a-message",children:"Send a message"}),"\n",(0,s.jsxs)(n.p,{children:["At any time, the server can send one or more messages to the client using its ",(0,s.jsx)(n.code,{children:"socket"})," object."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Server code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n createUser(ctx: WebsocketContext) {\n ctx.socket.emit('event 1', 'first message');\n ctx.socket.emit('event 1', 'second message');\n return new WebsocketResponse();\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Client code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"socket.on('event 1', payload => {\n console.log('Message: ', payload);\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"broadcast-a-message",children:"Broadcast a message"}),"\n",(0,s.jsxs)(n.p,{children:["If a message is to be broadcast to all clients, you can use the ",(0,s.jsx)(n.code,{children:"broadcast"})," property for this."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Server code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class UserController {\n\n @EventName('create')\n createUser(ctx: WebsocketContext) {\n ctx.socket.broadcast.emit('event 1', 'first message');\n ctx.socket.broadcast.emit('event 1', 'second message');\n return new WebsocketResponse();\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Client code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"socket.on('event 1', payload => {\n console.log('Message: ', payload);\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"grouping-clients-in-rooms",children:"Grouping clients in rooms"}),"\n",(0,s.jsxs)(n.p,{children:["Socket.io uses the concept of ",(0,s.jsx)(n.a,{href:"https://socket.io/docs/v4/rooms/",children:"rooms"})," to gather clients in groups. This can be useful if you need to send a message to a particular subset of clients."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n onConnection(ctx: WebsocketContext) {\n ctx.socket.join('some room');\n }\n\n @EventName('event 1')\n createUser(ctx: WebsocketContext) {\n ctx.socket.to('some room').emit('event 2');\n return new WebsocketResponse();\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"accessing-the-socketio-server",children:"Accessing the socket.io server"}),"\n",(0,s.jsxs)(n.p,{children:["You can access the socket.io server anywhere in your code (including your HTTP controllers) by injecting the ",(0,s.jsx)(n.code,{children:"WsServer"})," service."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { dependency, HttpResponseOK, Post } from '@foal/core';\nimport { WsServer } from '@foal/socket.io';\n\nexport class UserController {\n @dependency\n wsServer: WsServer;\n\n @Post('/users')\n createUser() {\n // ...\n this.wsServer.io.emit('refresh users');\n\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"error-handling",children:"Error-handling"}),"\n",(0,s.jsxs)(n.p,{children:["Any error thrown or rejected in a websocket controller, hook or service, if not caught, is converted to a ",(0,s.jsx)(n.code,{children:"WebsocketResponseError"}),". If the ",(0,s.jsx)(n.code,{children:"settings.debug"})," configuration parameter is ",(0,s.jsx)(n.code,{children:"true"}),", then the error is returned as is to the client. Otherwise, the server returns this response:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"({\n status: 'error',\n error: {\n code: 'INTERNAL_SERVER_ERROR',\n message: 'An internal server error has occurred.'\n }\n})\n"})}),"\n",(0,s.jsx)(n.h4,{id:"customizing-the-error-handler",children:"Customizing the error handler"}),"\n",(0,s.jsxs)(n.p,{children:["Just as its HTTP version, the ",(0,s.jsx)(n.code,{children:"SocketIOController"})," class supports an optional ",(0,s.jsx)(n.code,{children:"handleError"})," to override the default error handler."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, renderWebsocketError, SocketIOController, WebsocketContext, WebsocketErrorResponse } from '@foal/socket.io';\n\nclass PermissionDenied extends Error {}\n\nexport class WebsocketController extends SocketIOController implements ISocketIOController {\n @EventName('create user')\n createUser() {\n throw new PermissionDenied();\n }\n\n handleError(error: Error, ctx: WebsocketContext){\n if (error instanceof PermissionDenied) {\n return new WebsocketErrorResponse('Permission is denied');\n }\n\n return renderWebsocketError(error, ctx);\n }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"payload-validation",children:"Payload Validation"}),"\n",(0,s.jsxs)(n.p,{children:["Foal provides a default hook ",(0,s.jsx)(n.code,{children:"@ValidatePayload"})," to validate the request payload. It is very similar to its HTTP version ",(0,s.jsx)(n.code,{children:"@ValidateBody"}),"."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Server code"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n @EventName('create product')\n @ValidatePayload({\n additionalProperties: false,\n properties: { name: { type: 'string' }},\n required: [ 'name' ],\n type: 'object'\n })\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\n const product = new Product();\n product.name = payload.name;\n await product.save();\n\n // Send a message to all clients.\n ctx.socket.broadcast.emit('refresh products');\n return new WebsocketResponse();\n }\n\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"Validation error response"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"({\n status: 'error',\n error: {\n code: 'VALIDATION_PAYLOAD_ERROR',\n payload: [\n // errors\n ]\n }\n})\n"})}),"\n",(0,s.jsx)(n.h2,{id:"unit-testing",children:"Unit Testing"}),"\n",(0,s.jsxs)(n.p,{children:["Testing WebSocket controllers and hooks is very similar to testing their HTTP equivalent. The ",(0,s.jsx)(n.code,{children:"WebsocketContext"})," takes three parameters."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Name"}),(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"eventName"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"string"})}),(0,s.jsx)(n.td,{children:"The name of the event."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"payload"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"any"})}),(0,s.jsx)(n.td,{children:"The request payload."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"socket"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"any"})}),(0,s.jsxs)(n.td,{children:["The socket (optional). Default: ",(0,s.jsx)(n.code,{children:"{}"}),"."]})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,s.jsx)(n.h3,{id:"multiple-node-servers",children:"Multiple node servers"}),"\n",(0,s.jsx)(n.p,{children:"This example shows how to manage multiple node servers using a redis adapter."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install socket.io-adapter @socket.io/redis-adapter@8 redis@4\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"src/index.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { WebsocketController, pubClient, subClient } from './services/websocket.controller';\nasync function main() {\n const serviceManager = new ServiceManager();\n const app = await createApp(AppController, { serviceManager });\n const httpServer = http.createServer(app);\n // Connect the redis clients to the database.\n await Promise.all([pubClient.connect(), subClient.connect()]);\n await serviceManager.get(WebsocketController).attachHttpServer(httpServer);\n // ...\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"websocket.controller.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from '@foal/socket.io';\nimport { createAdapter } from '@socket.io/redis-adapter';\nimport { createClient } from 'redis';\n\nexport const pubClient = createClient({ url: 'redis://localhost:6379' });\nexport const subClient = pubClient.duplicate();\n\nexport class WebsocketController extends SocketIOController {\n adapter = createAdapter(pubClient, subClient);\n\n @EventName('create user')\n createUser(ctx: WebsocketContext) {\n // Broadcast an event to all clients of all servers.\n ctx.socket.broadcast.emit('refresh users');\n return new WebsocketResponse();\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"handling-connection",children:"Handling connection"}),"\n",(0,s.jsxs)(n.p,{children:["If you want to run some code when a Websocket connection is established (for example to join a room or forward the session), you can use the ",(0,s.jsx)(n.code,{children:"onConnection"})," method of the ",(0,s.jsx)(n.code,{children:"SocketIOController"})," for this."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { SocketIOController, WebsocketContext } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n onConnection(ctx: WebsocketContext) {\n // ...\n }\n\n}\n"})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["The context passed in the ",(0,s.jsx)(n.code,{children:"onConnection"})," method has an undefined payload and an empty event name."]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"error-handling-1",children:"Error-handling"}),"\n",(0,s.jsxs)(n.p,{children:["Any errors thrown or rejected in the ",(0,s.jsx)(n.code,{children:"onConnection"})," is sent back to the client. So you may need to add a ",(0,s.jsx)(n.code,{children:"try {} catch {}"})," in some cases."]}),"\n",(0,s.jsxs)(n.p,{children:["This error can be read on the client using the ",(0,s.jsx)(n.code,{children:"connect_error"})," event listener."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:'socket.on("connect_error", () => {\n // Do some stuff\n socket.connect();\n});\n'})}),"\n",(0,s.jsx)(n.h3,{id:"custom-server-options",children:"Custom server options"}),"\n",(0,s.jsxs)(n.p,{children:["Custom options can be passed to the socket.io server as follows. The complete list of options can be found ",(0,s.jsx)(n.a,{href:"https://socket.io/docs/v4/server-options/",children:"here"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { SocketIOController } from '@foal/socket.io';\n\nexport class WebsocketController extends SocketIOController {\n\n options = {\n connectTimeout: 60000\n }\n\n}\n"})})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>c,x:()=>i});var s=t(96540);const o={},r=s.createContext(o);function c(e){const n=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:c(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/a7023ddc.6cdf095f.js b/assets/js/a7023ddc.86b95db9.js similarity index 73% rename from assets/js/a7023ddc.6cdf095f.js rename to assets/js/a7023ddc.86b95db9.js index c2a0cac5df..858bde05e6 100644 --- a/assets/js/a7023ddc.6cdf095f.js +++ b/assets/js/a7023ddc.86b95db9.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9267],{28289:e=>{e.exports=JSON.parse('[{"label":"release","permalink":"/blog/tags/release","count":24},{"label":"survey","permalink":"/blog/tags/survey","count":1}]')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9267],{28289:e=>{e.exports=JSON.parse('[{"label":"release","permalink":"/blog/tags/release","count":25},{"label":"survey","permalink":"/blog/tags/survey","count":1}]')}}]); \ No newline at end of file diff --git a/assets/js/a74ed5d5.b6ed58da.js b/assets/js/a74ed5d5.b6ed58da.js new file mode 100644 index 0000000000..f64ee23cfa --- /dev/null +++ b/assets/js/a74ed5d5.b6ed58da.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[9162],{22779:(e,n,o)=>{o.r(n),o.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>c});var t=o(74848),s=o(28453);const i={title:"Version 4.5 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.5-release-notes.png",tags:["release"]},r=void 0,a={permalink:"/blog/2024/08/22/version-4.5-release-notes",editUrl:"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-08-22-version-4.5-release-notes.md",source:"@site/blog/2024-08-22-version-4.5-release-notes.md",title:"Version 4.5 release notes",description:"Banner",date:"2024-08-22T00:00:00.000Z",formattedDate:"August 22, 2024",tags:[{label:"release",permalink:"/blog/tags/release"}],readingTime:2.425,hasTruncateMarker:!0,authors:[{name:"Lo\xefc Poullain",title:"Creator of FoalTS. Software engineer.",url:"https://loicpoullain.com",imageURL:"https://avatars1.githubusercontent.com/u/13604533?v=4"}],frontMatter:{title:"Version 4.5 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.5-release-notes.png",tags:["release"]},unlisted:!1,nextItem:{title:"Version 4.4 release notes",permalink:"/blog/2024/04/25/version-4.4-release-notes"}},l={authorsImageUrls:[void 0]},c=[{value:"Asynchronous tasks",id:"asynchronous-tasks",level:2},{value:"Social authentication for SPAs",id:"social-authentication-for-spas",level:2},{value:"Google social authentification",id:"google-social-authentification",level:2},{value:"Logging improvements",id:"logging-improvements",level:2},{value:"CLI fixes",id:"cli-fixes",level:2},{value:"Global use of CLI deprecated",id:"global-use-of-cli-deprecated",level:2}];function d(e){const n={a:"a",code:"code",h2:"h2",img:"img",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Banner",src:o(60572).A+"",width:"914",height:"315"})}),"\n",(0,t.jsxs)(n.p,{children:["Version 4.5 of ",(0,t.jsx)(n.a,{href:"https://foalts.org/",children:"Foal"})," is out!"]}),"\n",(0,t.jsx)(n.h2,{id:"asynchronous-tasks",children:"Asynchronous tasks"}),"\n",(0,t.jsx)(n.p,{children:"In some situations, we need to execute a specific task without waiting for it and without blocking the request."}),"\n",(0,t.jsx)(n.p,{children:"This could be, for example, sending a specific message to the CRM or company chat. In this case, the user needs to be able to see his or her request completed as quickly as possible, even if the request to the CRM takes some time or fails."}),"\n",(0,t.jsxs)(n.p,{children:["To this end, Foal version 4.5 provides an ",(0,t.jsx)(n.code,{children:"AsyncService"})," to execute these tasks asynchronously, and correctly catch and log their errors where appropriate."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { AsyncService, dependency } from '@foal/core';\n\nimport { CRMService } from './somewhere';\n\nexport class SubscriptionService {\n @dependency\n asyncService: AsyncService;\n\n @dependency\n crmService: CRMService;\n\n async subscribe(userId: number): Promise {\n // Do something\n\n this.asyncService.run(() => this.crmService.updateUser(userId));\n }\n}\n\n"})}),"\n",(0,t.jsx)(n.h2,{id:"social-authentication-for-spas",children:"Social authentication for SPAs"}),"\n",(0,t.jsxs)(n.p,{children:["If you wish to manually manage the redirection to the consent page on the client side (which is often necessary when developing an SPA), you can now do so with the ",(0,t.jsx)(n.code,{children:"createHttpResponseWithConsentPageUrl"})," method. It returns an ",(0,t.jsx)(n.code,{children:"HttpResponseOK"})," whose body contains the URL of the consent page."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"export class AuthController {\n @dependency\n google: GoogleProvider;\n\n @Get('/signin/google')\n getConsentPageURL() {\n return this.google.createHttpResponseWithConsentPageUrl();\n }\n \n // ...\n\n}\n\n"})}),"\n",(0,t.jsx)(n.h2,{id:"google-social-authentification",children:"Google social authentification"}),"\n",(0,t.jsxs)(n.p,{children:["The typing of the ",(0,t.jsx)(n.code,{children:"GoogleProvider"})," service has been improved. The ",(0,t.jsx)(n.code,{children:"userInfo"})," property returned by ",(0,t.jsx)(n.code,{children:"getUserInfo"})," is now typed with the values returned by the Google server."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const { userInfo } = await this.googleProvider.getUserInfo(...);\n\n// userInfo.email, userInfo.family_name, etc\n"})}),"\n",(0,t.jsx)(n.h2,{id:"logging-improvements",children:"Logging improvements"}),"\n",(0,t.jsxs)(n.p,{children:["In previous versions, the util function ",(0,t.jsx)(n.code,{children:"displayServerURL"})," and configuration errors printed logs on several lines, which was not appropriate for logging software."]}),"\n",(0,t.jsxs)(n.p,{children:["From version 4.5 onwards, configuration errors are displayed on a single line and the ",(0,t.jsx)(n.code,{children:"displayServerURL"})," function is marked as deprecated."]}),"\n",(0,t.jsx)(n.h2,{id:"cli-fixes",children:"CLI fixes"}),"\n",(0,t.jsxs)(n.p,{children:["When running ",(0,t.jsx)(n.code,{children:"npx foal connect react"})," to connect the React application to the Foal application in development, the following features did not work:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Proxify requests from the client to the server without needing to enable CORS or specify a different port in development."}),"\n",(0,t.jsx)(n.li,{children:"Build the client application in the server application's public directory."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"This is fixed in v4.5."}),"\n",(0,t.jsx)(n.h2,{id:"global-use-of-cli-deprecated",children:"Global use of CLI deprecated"}),"\n",(0,t.jsx)(n.p,{children:"In previous versions, the tutorial suggested installing the CLI globally to create a new application or generate files. However, it is considered bad practice to install a dependency globally for local use."}),"\n",(0,t.jsx)(n.p,{children:"In addition, the CLI was also installed locally so that the build command would work when deploying the application to a CI or to production. This was maintaining two versions of the CLI."}),"\n",(0,t.jsxs)(n.p,{children:["To correct this, in the documentation and examples, the CLI is now always installed and used locally. To use it, simply add ",(0,t.jsx)(n.code,{children:"npx"})," before each command (except for ",(0,t.jsx)(n.code,{children:"createapp"}),")."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"# Before\nfoal createapp my-app\n\nfoal generate script foobar\nfoal createsecret\n\n# After\nnpx @foal/cli createapp my-app\n\nnpx foal generate script foobar\nnpx foal createsecret\n"})})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},60572:(e,n,o)=>{o.d(n,{A:()=>t});const t=o.p+"assets/images/banner-3f2086c61f3c010fc38db9701cd3d398.png"},28453:(e,n,o)=>{o.d(n,{R:()=>r,x:()=>a});var t=o(96540);const s={},i=t.createContext(s);function r(e){const n=t.useContext(i);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/ad0d29b1.76817fde.js b/assets/js/ad0d29b1.76817fde.js new file mode 100644 index 0000000000..06dcf48505 --- /dev/null +++ b/assets/js/ad0d29b1.76817fde.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2072],{67834:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>h,frontMatter:()=>r,metadata:()=>i,toc:()=>l});var o=t(74848),a=t(28453);const r={title:"Angular, React & Vue"},d=void 0,i={id:"frontend/angular-react-vue",title:"Angular, React & Vue",description:"Angular, React and Vue all provide powerful CLIs for creating frontend applications. These tools are widely used, regularly improved and extensively documented. That's why Foal CLI do not provide ready-made features to build the frontend in their place.",source:"@site/docs/frontend/angular-react-vue.md",sourceDirName:"frontend",slug:"/frontend/angular-react-vue",permalink:"/docs/frontend/angular-react-vue",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/frontend/angular-react-vue.md",tags:[],version:"current",frontMatter:{title:"Angular, React & Vue"},sidebar:"someSidebar",previous:{title:"Single Page Applications",permalink:"/docs/frontend/single-page-applications"},next:{title:"Server-Side Rendering",permalink:"/docs/frontend/server-side-rendering"}},c={},l=[{value:"Creating a new Application",id:"creating-a-new-application",level:2},{value:"Angular",id:"angular",level:3},{value:"React",id:"react",level:3},{value:"Vue",id:"vue",level:3},{value:"Problems Solved by the connect Command",id:"problems-solved-by-the-connect-command",level:2},{value:"Origins that Do not Match",id:"origins-that-do-not-match",level:3},{value:"Build Outpath",id:"build-outpath",level:3}];function s(e){const n={code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,a.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"npx foal connect angular ../frontend\nnpx foal connect react ../frontend\nnpx foal connect vue ../frontend\n"})}),"\n",(0,o.jsx)(n.p,{children:"Angular, React and Vue all provide powerful CLIs for creating frontend applications. These tools are widely used, regularly improved and extensively documented. That's why Foal CLI do not provide ready-made features to build the frontend in their place."}),"\n",(0,o.jsxs)(n.p,{children:["Instead, FoalTS offers a convenient command, named ",(0,o.jsx)(n.code,{children:"connect"}),", to configure your frontend CLI so that it interacts smoothly with your Foal application. This way, you do not have to worry about the details of the configuration when starting a new project. You can leave this until later if you need it."]}),"\n",(0,o.jsx)(n.h2,{id:"creating-a-new-application",children:"Creating a new Application"}),"\n",(0,o.jsx)(n.h3,{id:"angular",children:"Angular"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"mkdir my-app\ncd my-app\n\nnpx @foal/cli createapp backend\nng new frontend\n\ncd backend\nnpx foal connect angular ../frontend\n"})}),"\n",(0,o.jsx)(n.h3,{id:"react",children:"React"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"mkdir my-app\ncd my-app\n\nnpx @foal/cli createapp backend\nnpx create-react-app frontend --template typescript\n\ncd backend\nnpx foal connect react ../frontend\n"})}),"\n",(0,o.jsx)(n.h3,{id:"vue",children:"Vue"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"mkdir my-app\ncd my-app\n\nnpx @foal/cli createapp backend\nvue create frontend\n\ncd backend\nnpx foal connect vue ../frontend\n"})}),"\n",(0,o.jsxs)(n.h2,{id:"problems-solved-by-the-connect-command",children:["Problems Solved by the ",(0,o.jsx)(n.code,{children:"connect"})," Command"]}),"\n",(0,o.jsx)(n.h3,{id:"origins-that-do-not-match",children:"Origins that Do not Match"}),"\n",(0,o.jsxs)(n.p,{children:["When building a web application with a Angular / React / Vue, it is very common in development to have two servers serving on different ports. For example, with an application written in Foal and Angular, the backend server serves the port ",(0,o.jsx)(n.code,{children:"3001"})," and the frontend one servers the ",(0,o.jsx)(n.code,{children:"4200"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["Consequently requests made by the frontend do not reach the backend as they have a different origin. One hacky solution is to replace the URL path to ",(0,o.jsx)(n.code,{children:"http://localhost:3001"})," in development and to enable CORS requests."]}),"\n",(0,o.jsx)(n.p,{children:"This technique has some drawbacks however:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"It may introduce a different codebase between the environments (dev and prod)."}),"\n",(0,o.jsxs)(n.li,{children:["And it disables a browser protection (the ",(0,o.jsx)(n.code,{children:"Same-Origin policy"}),")."]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["One way to get around this, keeping the policy and the same codebase, is to configure a proxy to redirect ",(0,o.jsx)(n.code,{children:"4200"})," requests to the port ",(0,o.jsx)(n.code,{children:"3001"}),". The ",(0,o.jsx)(n.code,{children:"connect"})," command does it for you."]}),"\n",(0,o.jsx)(n.h3,{id:"build-outpath",children:"Build Outpath"}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"connect"})," command also modifies the build output path of your front so that its bundles are saved in the ",(0,o.jsx)(n.code,{children:"public/"})," directory. This way, you can run the frontend and the backend build commands and directly ship the application to production."]})]})}function h(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(s,{...e})}):s(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>d,x:()=>i});var o=t(96540);const a={},r=o.createContext(a);function d(e){const n=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:d(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/ad0d29b1.ee498ccf.js b/assets/js/ad0d29b1.ee498ccf.js deleted file mode 100644 index db9177a247..0000000000 --- a/assets/js/ad0d29b1.ee498ccf.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2072],{67834:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var o=t(74848),r=t(28453);const a={title:"Angular, React & Vue"},d=void 0,i={id:"frontend/angular-react-vue",title:"Angular, React & Vue",description:"Angular, React and Vue all provide powerful CLIs for creating frontend applications. These tools are widely used, regularly improved and extensively documented. That's why Foal CLI do not provide ready-made features to build the frontend in their place.",source:"@site/docs/frontend/angular-react-vue.md",sourceDirName:"frontend",slug:"/frontend/angular-react-vue",permalink:"/docs/frontend/angular-react-vue",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/frontend/angular-react-vue.md",tags:[],version:"current",frontMatter:{title:"Angular, React & Vue"},sidebar:"someSidebar",previous:{title:"Single Page Applications",permalink:"/docs/frontend/single-page-applications"},next:{title:"Server-Side Rendering",permalink:"/docs/frontend/server-side-rendering"}},c={},l=[{value:"Creating a new Application",id:"creating-a-new-application",level:2},{value:"Angular",id:"angular",level:3},{value:"React",id:"react",level:3},{value:"Vue",id:"vue",level:3},{value:"Problems Solved by the connect Command",id:"problems-solved-by-the-connect-command",level:2},{value:"Origins that Do not Match",id:"origins-that-do-not-match",level:3},{value:"Build Outpath",id:"build-outpath",level:3}];function s(e){const n={blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"foal connect angular ../frontend\nfoal connect react ../frontend\nfoal connect vue ../frontend\n"})}),"\n",(0,o.jsx)(n.p,{children:"Angular, React and Vue all provide powerful CLIs for creating frontend applications. These tools are widely used, regularly improved and extensively documented. That's why Foal CLI do not provide ready-made features to build the frontend in their place."}),"\n",(0,o.jsxs)(n.p,{children:["Instead, FoalTS offers a convenient command, named ",(0,o.jsx)(n.code,{children:"connect"}),", to configure your frontend CLI so that it interacts smoothly with your Foal application. This way, you do not have to worry about the details of the configuration when starting a new project. You can leave this until later if you need it."]}),"\n",(0,o.jsx)(n.h2,{id:"creating-a-new-application",children:"Creating a new Application"}),"\n",(0,o.jsx)(n.h3,{id:"angular",children:"Angular"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"mkdir my-app\ncd my-app\n\nfoal createapp backend\nng new frontend\n\ncd backend\nfoal connect angular ../frontend\n"})}),"\n",(0,o.jsx)(n.h3,{id:"react",children:"React"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"mkdir my-app\ncd my-app\n\nfoal createapp backend\nnpx create-react-app frontend --template typescript\n\ncd backend\nfoal connect react ../frontend\n"})}),"\n",(0,o.jsx)(n.h3,{id:"vue",children:"Vue"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"mkdir my-app\ncd my-app\n\nfoal createapp backend\nvue create frontend\n\ncd backend\nfoal connect vue ../frontend\n"})}),"\n",(0,o.jsxs)(n.h2,{id:"problems-solved-by-the-connect-command",children:["Problems Solved by the ",(0,o.jsx)(n.code,{children:"connect"})," Command"]}),"\n",(0,o.jsx)(n.h3,{id:"origins-that-do-not-match",children:"Origins that Do not Match"}),"\n",(0,o.jsxs)(n.p,{children:["When building a web application with a Angular / React / Vue, it is very common in development to have two servers serving on different ports. For example, with an application written in Foal and Angular, the backend server serves the port ",(0,o.jsx)(n.code,{children:"3001"})," and the frontend one servers the ",(0,o.jsx)(n.code,{children:"4200"}),"."]}),"\n",(0,o.jsxs)(n.p,{children:["Consequently requests made by the frontend do not reach the backend as they have a different origin. One hacky solution is to replace the URL path to ",(0,o.jsx)(n.code,{children:"http://localhost:3001"})," in development and to enable CORS requests."]}),"\n",(0,o.jsx)(n.p,{children:"This technique has some drawbacks however:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"It may introduce a different codebase between the environments (dev and prod)."}),"\n",(0,o.jsxs)(n.li,{children:["And it disables a browser protection (the ",(0,o.jsx)(n.code,{children:"Same-Origin policy"}),")."]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["One way to get around this, keeping the policy and the same codebase, is to configure a proxy to redirect ",(0,o.jsx)(n.code,{children:"4200"})," requests to the port ",(0,o.jsx)(n.code,{children:"3001"}),". The ",(0,o.jsx)(n.code,{children:"connect"})," command does it for you."]}),"\n",(0,o.jsx)(n.h3,{id:"build-outpath",children:"Build Outpath"}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"This feature only works with Angular and Vue."})}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["The ",(0,o.jsx)(n.code,{children:"connect"})," command also modifies the build output path of your front so that its bundles are saved in the ",(0,o.jsx)(n.code,{children:"public/"})," directory. This way, you can run the frontend and the backend build commands and directly ship the application to production."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(s,{...e})}):s(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>d,x:()=>i});var o=t(96540);const r={},a=o.createContext(r);function d(e){const n=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),o.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/add410f2.7d44401c.js b/assets/js/add410f2.7d44401c.js new file mode 100644 index 0000000000..2ab5e840bd --- /dev/null +++ b/assets/js/add410f2.7d44401c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5271],{27920:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>p,frontMatter:()=>o,metadata:()=>c,toc:()=>a});var s=r(74848),t=r(28453);const o={title:"gRPC"},i=void 0,c={id:"common/gRPC",title:"gRPC",description:"gRPC is a Remote Procedure Call (RPC) framework that can run in any environment. It can connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.",source:"@site/docs/common/gRPC.md",sourceDirName:"common",slug:"/common/gRPC",permalink:"/docs/common/gRPC",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/gRPC.md",tags:[],version:"current",frontMatter:{title:"gRPC"},sidebar:"someSidebar",previous:{title:"WebSockets",permalink:"/docs/common/websockets"},next:{title:"Utilities",permalink:"/docs/common/utilities"}},l={},a=[{value:"Installation & Configuration",id:"installation--configuration",level:2},{value:"The gRPC Service",id:"the-grpc-service",level:2},{value:"Using Promises",id:"using-promises",level:2},{value:"Limitations",id:"limitations",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"gRPC is a Remote Procedure Call (RPC) framework that can run in any environment. It can connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services."}),"\n",(0,s.jsxs)(n.p,{children:["This page shows how to use gRPC in Foal. It is based on the ",(0,s.jsx)(n.a,{href:"https://grpc.io/docs/languages/node/basics/",children:"official gRPC tutorial"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"installation--configuration",children:"Installation & Configuration"}),"\n",(0,s.jsx)(n.p,{children:"First you need to install some additional dependencies."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install @grpc/grpc-js @grpc/proto-loader\nnpm install cpx2 --save-dev\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Then update your ",(0,s.jsx)(n.code,{children:"package.json"})," so that your build scripts will correctly copy your ",(0,s.jsx)(n.code,{children:".proto"})," files into the ",(0,s.jsx)(n.code,{children:"build/"})," directory."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "build": "npx foal rmdir build && cpx \\"src/**/*.proto\\" build && tsc -p tsconfig.app.json",\n "dev": "npm run build && concurrently \\"cpx \\\\\\"src/**/*.proto\\\\\\" build -w\\" \\"tsc -p tsconfig.app.json -w\\" \\"supervisor -w ./build,./config -e js,json,yml,proto --no-restart-on error ./build/index.js\\"",\n "build:test": "npx foal rmdir build && cpx \\"src/**/*.proto\\" build && tsc -p tsconfig.test.json",\n "test": "npm run build:test && concurrently \\"cpx \\\\\\"src/**/*.proto\\\\\\" build -w\\" \\"tsc -p tsconfig.test.json -w\\" \\"mocha --file ./build/test.js -w --watch-files build \\\\\\"./build/**/*.spec.js\\\\\\"\\"",\n "build:e2e": "npx foal rmdir build && cpx \\"src/**/*.proto\\" build && tsc -p tsconfig.e2e.json",\n "e2e": "npm run build:e2e && concurrently \\"cpx \\\\\\"src/**/*.proto\\\\\\" build -w\\" \\"tsc -p tsconfig.e2e.json -w\\" \\"mocha --file ./build/e2e.js -w --watch-files build \\\\\\"./build/e2e/**/*.js\\\\\\"\\"",\n ...\n}\n'})}),"\n",(0,s.jsxs)(n.h2,{id:"the-grpc-service",children:["The ",(0,s.jsx)(n.code,{children:"gRPC"})," Service"]}),"\n",(0,s.jsxs)(n.p,{children:["Create your ",(0,s.jsx)(n.code,{children:"spec.proto"})," file and save it to ",(0,s.jsx)(n.code,{children:"src/app"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-proto",children:'syntax = "proto3";\n\npackage helloworld;\n\n// The greeting service definition.\nservice Greeter {\n // Sends a greeting\n rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\n// The request message containing the user\'s name.\nmessage HelloRequest {\n string name = 1;\n}\n\n// The response message containing the greetings\nmessage HelloReply {\n string message = 1;\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Add the ",(0,s.jsx)(n.code,{children:"Greeter"})," service."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"services/greeter.service.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"export class Greeter {\n\n sayHello(call, callback) {\n callback(null, {message: 'Hello ' + call.request.name});\n }\n\n}\n\n"})}),"\n",(0,s.jsx)(n.p,{children:"The next step is to create a service that will instantiate the gRPC server."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"services/grpc.service.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { join } from 'path';\n\n// 3p\nimport { dependency } from '@foal/core';\nimport * as grpc from '@grpc/grpc-js';\nimport * as protoLoader from '@grpc/proto-loader';\n\n// App\nimport { Greeter } from './greeter.service';\n\nexport class Grpc {\n @dependency\n greeter: Greeter;\n\n boot(): Promise {\n const PROTO_PATH = join(__dirname, '../spec.proto');\n const packageDefinition = protoLoader.loadSync(\n PROTO_PATH,\n {\n keepCase: true,\n longs: String,\n enums: String,\n defaults: true,\n oneofs: true\n }\n );\n\n const helloProto: any = grpc.loadPackageDefinition(packageDefinition).helloworld;\n const server = new grpc.Server();\n server.addService(helloProto.Greeter.service, this.greeter as any);\n // OR\n // server.addService(helloProto.Greeter.service, {\n // sayHello: this.greeter.sayHello.bind(this.greeter)\n // } as any);\n\n return new Promise((resolve, reject) => {\n server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), error => {\n if (error) {\n return reject(error);\n }\n server.start();\n return resolve();\n });\n })\n }\n\n}\n\n"})}),"\n",(0,s.jsx)(n.p,{children:"Finally, inject the service in the application."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"export class AppController {\n @dependency\n grpc: Grpc;\n\n // ...\n}\n\n"})}),"\n",(0,s.jsx)(n.h2,{id:"using-promises",children:"Using Promises"}),"\n",(0,s.jsx)(n.p,{children:"Using callbacks in the grpc services can be quite tedious. To convert methods into functions that use promises, you can use the decorator below."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { callbackify } from 'util';\n\nfunction async (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n descriptor.value = callbackify(descriptor.value);\n};\n\nexport class Greeter {\n\n @async\n async sayHello(call) {\n return { message: 'Hello ' + call.request.name };\n }\n\n}\n\n"})}),"\n",(0,s.jsx)(n.h2,{id:"limitations",children:"Limitations"}),"\n",(0,s.jsx)(n.p,{children:"The implementation above do not use Foal regular controllers. This has two consequences:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"hooks cannot be used,"}),"\n",(0,s.jsxs)(n.li,{children:["and error-handling is entirely managed by the gRPC server. The ",(0,s.jsx)(n.code,{children:"AppController.handleError"})," cannot be used."]}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453:(e,n,r)=>{r.d(n,{R:()=>i,x:()=>c});var s=r(96540);const t={},o=s.createContext(t);function i(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/add410f2.e72bfcbe.js b/assets/js/add410f2.e72bfcbe.js deleted file mode 100644 index 4edde4fb35..0000000000 --- a/assets/js/add410f2.e72bfcbe.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5271],{27920:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>i,default:()=>p,frontMatter:()=>o,metadata:()=>c,toc:()=>a});var s=r(74848),t=r(28453);const o={title:"gRPC"},i=void 0,c={id:"common/gRPC",title:"gRPC",description:"gRPC is a Remote Procedure Call (RPC) framework that can run in any environment. It can connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.",source:"@site/docs/common/gRPC.md",sourceDirName:"common",slug:"/common/gRPC",permalink:"/docs/common/gRPC",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/gRPC.md",tags:[],version:"current",frontMatter:{title:"gRPC"},sidebar:"someSidebar",previous:{title:"WebSockets",permalink:"/docs/common/websockets"},next:{title:"Utilities",permalink:"/docs/common/utilities"}},l={},a=[{value:"Installation & Configuration",id:"installation--configuration",level:2},{value:"The gRPC Service",id:"the-grpc-service",level:2},{value:"Using Promises",id:"using-promises",level:2},{value:"Limitations",id:"limitations",level:2}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"gRPC is a Remote Procedure Call (RPC) framework that can run in any environment. It can connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services."}),"\n",(0,s.jsxs)(n.p,{children:["This page shows how to use gRPC in Foal. It is based on the ",(0,s.jsx)(n.a,{href:"https://grpc.io/docs/languages/node/basics/",children:"official gRPC tutorial"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"installation--configuration",children:"Installation & Configuration"}),"\n",(0,s.jsx)(n.p,{children:"First you need to install some additional dependencies."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm install @grpc/grpc-js @grpc/proto-loader\nnpm install cpx2 --save-dev\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Then update your ",(0,s.jsx)(n.code,{children:"package.json"})," so that your build scripts will correctly copy your ",(0,s.jsx)(n.code,{children:".proto"})," files into the ",(0,s.jsx)(n.code,{children:"build/"})," directory."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "build": "foal rmdir build && cpx \\"src/**/*.proto\\" build && tsc -p tsconfig.app.json",\n "dev": "npm run build && concurrently \\"cpx \\\\\\"src/**/*.proto\\\\\\" build -w\\" \\"tsc -p tsconfig.app.json -w\\" \\"supervisor -w ./build,./config -e js,json,yml,proto --no-restart-on error ./build/index.js\\"",\n "build:test": "foal rmdir build && cpx \\"src/**/*.proto\\" build && tsc -p tsconfig.test.json",\n "test": "npm run build:test && concurrently \\"cpx \\\\\\"src/**/*.proto\\\\\\" build -w\\" \\"tsc -p tsconfig.test.json -w\\" \\"mocha --file ./build/test.js -w --watch-files build \\\\\\"./build/**/*.spec.js\\\\\\"\\"",\n "build:e2e": "foal rmdir build && cpx \\"src/**/*.proto\\" build && tsc -p tsconfig.e2e.json",\n "e2e": "npm run build:e2e && concurrently \\"cpx \\\\\\"src/**/*.proto\\\\\\" build -w\\" \\"tsc -p tsconfig.e2e.json -w\\" \\"mocha --file ./build/e2e.js -w --watch-files build \\\\\\"./build/e2e/**/*.js\\\\\\"\\"",\n ...\n}\n'})}),"\n",(0,s.jsxs)(n.h2,{id:"the-grpc-service",children:["The ",(0,s.jsx)(n.code,{children:"gRPC"})," Service"]}),"\n",(0,s.jsxs)(n.p,{children:["Create your ",(0,s.jsx)(n.code,{children:"spec.proto"})," file and save it to ",(0,s.jsx)(n.code,{children:"src/app"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-proto",children:'syntax = "proto3";\n\npackage helloworld;\n\n// The greeting service definition.\nservice Greeter {\n // Sends a greeting\n rpc SayHello (HelloRequest) returns (HelloReply) {}\n}\n\n// The request message containing the user\'s name.\nmessage HelloRequest {\n string name = 1;\n}\n\n// The response message containing the greetings\nmessage HelloReply {\n string message = 1;\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Add the ",(0,s.jsx)(n.code,{children:"Greeter"})," service."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"services/greeter.service.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"export class Greeter {\n\n sayHello(call, callback) {\n callback(null, {message: 'Hello ' + call.request.name});\n }\n\n}\n\n"})}),"\n",(0,s.jsx)(n.p,{children:"The next step is to create a service that will instantiate the gRPC server."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.em,{children:"services/grpc.service.ts"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { join } from 'path';\n\n// 3p\nimport { dependency } from '@foal/core';\nimport * as grpc from '@grpc/grpc-js';\nimport * as protoLoader from '@grpc/proto-loader';\n\n// App\nimport { Greeter } from './greeter.service';\n\nexport class Grpc {\n @dependency\n greeter: Greeter;\n\n boot(): Promise {\n const PROTO_PATH = join(__dirname, '../spec.proto');\n const packageDefinition = protoLoader.loadSync(\n PROTO_PATH,\n {\n keepCase: true,\n longs: String,\n enums: String,\n defaults: true,\n oneofs: true\n }\n );\n\n const helloProto: any = grpc.loadPackageDefinition(packageDefinition).helloworld;\n const server = new grpc.Server();\n server.addService(helloProto.Greeter.service, this.greeter as any);\n // OR\n // server.addService(helloProto.Greeter.service, {\n // sayHello: this.greeter.sayHello.bind(this.greeter)\n // } as any);\n\n return new Promise((resolve, reject) => {\n server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), error => {\n if (error) {\n return reject(error);\n }\n server.start();\n return resolve();\n });\n })\n }\n\n}\n\n"})}),"\n",(0,s.jsx)(n.p,{children:"Finally, inject the service in the application."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"export class AppController {\n @dependency\n grpc: Grpc;\n\n // ...\n}\n\n"})}),"\n",(0,s.jsx)(n.h2,{id:"using-promises",children:"Using Promises"}),"\n",(0,s.jsx)(n.p,{children:"Using callbacks in the grpc services can be quite tedious. To convert methods into functions that use promises, you can use the decorator below."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { callbackify } from 'util';\n\nfunction async (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n descriptor.value = callbackify(descriptor.value);\n};\n\nexport class Greeter {\n\n @async\n async sayHello(call) {\n return { message: 'Hello ' + call.request.name };\n }\n\n}\n\n"})}),"\n",(0,s.jsx)(n.h2,{id:"limitations",children:"Limitations"}),"\n",(0,s.jsx)(n.p,{children:"The implementation above do not use Foal regular controllers. This has two consequences:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"hooks cannot be used,"}),"\n",(0,s.jsxs)(n.li,{children:["and error-handling is entirely managed by the gRPC server. The ",(0,s.jsx)(n.code,{children:"AppController.handleError"})," cannot be used."]}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},28453:(e,n,r)=>{r.d(n,{R:()=>i,x:()=>c});var s=r(96540);const t={},o=s.createContext(t);function i(e){const n=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/aef2f35b.49cdca7a.js b/assets/js/aef2f35b.2d4721b1.js similarity index 68% rename from assets/js/aef2f35b.49cdca7a.js rename to assets/js/aef2f35b.2d4721b1.js index 995e784d54..f42ff9082f 100644 --- a/assets/js/aef2f35b.49cdca7a.js +++ b/assets/js/aef2f35b.2d4721b1.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[6365],{21605:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>l});var d=n(74848),s=n(28453);const o={title:"Commands"},r=void 0,i={id:"cli/commands",title:"Commands",description:"FoalTS provides several commands to help you build and develop your app.",source:"@site/docs/cli/commands.md",sourceDirName:"cli",slug:"/cli/commands",permalink:"/docs/cli/commands",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/cli/commands.md",tags:[],version:"current",frontMatter:{title:"Commands"},sidebar:"someSidebar",previous:{title:"404 Page",permalink:"/docs/frontend/not-found-page"},next:{title:"Shell Scripts",permalink:"/docs/cli/shell-scripts"}},c={},l=[];function a(e){const t={code:"code",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,s.R)(),...e.components};return(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(t.p,{children:"FoalTS provides several commands to help you build and develop your app."}),"\n",(0,d.jsxs)(t.table,{children:[(0,d.jsx)(t.thead,{children:(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.th,{children:"Command"}),(0,d.jsx)(t.th,{children:"Description"})]})}),(0,d.jsxs)(t.tbody,{children:[(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npm run dev"})}),(0,d.jsxs)(t.td,{children:["Build the source code and start the server. If a file changes then the code is rebuilt and the server reloads. This is usually ",(0,d.jsx)(t.strong,{children:"the only command that you need during development"})]})]}),(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npm run build"})}),(0,d.jsxs)(t.td,{children:["Build the app code located in the ",(0,d.jsx)(t.code,{children:"src/"})," directory (test files are ignored)."]})]}),(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npm run start"})}),(0,d.jsx)(t.td,{children:"Start the server from the built files."})]}),(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"foal upgrade [version]"})}),(0,d.jsxs)(t.td,{children:["Upgrade all local ",(0,d.jsx)(t.code,{children:"@foal/*"})," dependencies and dev dependencies to the given version. If no version is provided, then the command upgrades to the latest version of Foal. An additional flag ",(0,d.jsx)(t.code,{children:"--no-install"})," can be provided to not trigger the npm or yarn installation."]})]})]})]})]})}function h(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,d.jsx)(t,{...e,children:(0,d.jsx)(a,{...e})}):a(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>r,x:()=>i});var d=n(96540);const s={},o=d.createContext(s);function r(e){const t=d.useContext(o);return d.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),d.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[6365],{21605:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>l});var d=n(74848),s=n(28453);const o={title:"Commands"},r=void 0,i={id:"cli/commands",title:"Commands",description:"FoalTS provides several commands to help you build and develop your app.",source:"@site/docs/cli/commands.md",sourceDirName:"cli",slug:"/cli/commands",permalink:"/docs/cli/commands",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/cli/commands.md",tags:[],version:"current",frontMatter:{title:"Commands"},sidebar:"someSidebar",previous:{title:"404 Page",permalink:"/docs/frontend/not-found-page"},next:{title:"Shell Scripts",permalink:"/docs/cli/shell-scripts"}},c={},l=[];function a(e){const t={code:"code",p:"p",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,s.R)(),...e.components};return(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(t.p,{children:"FoalTS provides several commands to help you build and develop your app."}),"\n",(0,d.jsxs)(t.table,{children:[(0,d.jsx)(t.thead,{children:(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.th,{children:"Command"}),(0,d.jsx)(t.th,{children:"Description"})]})}),(0,d.jsxs)(t.tbody,{children:[(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npm run dev"})}),(0,d.jsxs)(t.td,{children:["Build the source code and start the server. If a file changes then the code is rebuilt and the server reloads. This is usually ",(0,d.jsx)(t.strong,{children:"the only command that you need during development"})]})]}),(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npm run build"})}),(0,d.jsxs)(t.td,{children:["Build the app code located in the ",(0,d.jsx)(t.code,{children:"src/"})," directory (test files are ignored)."]})]}),(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npm run start"})}),(0,d.jsx)(t.td,{children:"Start the server from the built files."})]}),(0,d.jsxs)(t.tr,{children:[(0,d.jsx)(t.td,{children:(0,d.jsx)(t.code,{children:"npx foal upgrade [version]"})}),(0,d.jsxs)(t.td,{children:["Upgrade all local ",(0,d.jsx)(t.code,{children:"@foal/*"})," dependencies and dev dependencies to the given version. If no version is provided, then the command upgrades to the latest version of Foal. An additional flag ",(0,d.jsx)(t.code,{children:"--no-install"})," can be provided to not trigger the npm or yarn installation."]})]})]})]})]})}function h(e={}){const{wrapper:t}={...(0,s.R)(),...e.components};return t?(0,d.jsx)(t,{...e,children:(0,d.jsx)(a,{...e})}):a(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>r,x:()=>i});var d=n(96540);const s={},o=d.createContext(s);function r(e){const t=d.useContext(o);return d.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),d.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b03290eb.bbf867f4.js b/assets/js/b03290eb.bbf867f4.js new file mode 100644 index 0000000000..e680bfc675 --- /dev/null +++ b/assets/js/b03290eb.bbf867f4.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[80],{23035:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>h});var i=t(74848),r=t(28453),s=t(11470),o=t(19365);const a={title:"Social Authentication",sidebar_label:"Social Auth"},l=void 0,c={id:"authentication/social-auth",title:"Social Authentication",description:"In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:",source:"@site/docs/authentication/social-auth.md",sourceDirName:"authentication",slug:"/authentication/social-auth",permalink:"/docs/authentication/social-auth",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/social-auth.md",tags:[],version:"current",frontMatter:{title:"Social Authentication",sidebar_label:"Social Auth"},sidebar:"someSidebar",previous:{title:"JSON Web Tokens",permalink:"/docs/authentication/jwt"},next:{title:"Administrators & Roles",permalink:"/docs/authorization/administrators-and-roles"}},d={},h=[{value:"Get Started",id:"get-started",level:2},{value:"General overview",id:"general-overview",level:3},{value:"Registering an application",id:"registering-an-application",level:3},{value:"Installation and configuration",id:"installation-and-configuration",level:3},{value:"Adding controllers",id:"adding-controllers",level:3},{value:"Techniques",id:"techniques",level:2},{value:"Usage with sessions",id:"usage-with-sessions",level:3},{value:"Usage with JWT",id:"usage-with-jwt",level:3},{value:"Custom Provider",id:"custom-provider",level:2},{value:"Sending the Client Credentials in an Authorization Header",id:"sending-the-client-credentials-in-an-authorization-header",level:3},{value:"Enabling Code Flow with PKCE",id:"enabling-code-flow-with-pkce",level:3},{value:"Official Providers",id:"official-providers",level:2},{value:"Google",id:"google",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application",level:4},{value:"Redirection parameters",id:"redirection-parameters",level:4},{value:"Facebook",id:"facebook",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-1",level:4},{value:"Redirection parameters",id:"redirection-parameters-1",level:4},{value:"User info parameters",id:"user-info-parameters",level:4},{value:"Github",id:"github",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-2",level:4},{value:"Redirection parameters",id:"redirection-parameters-2",level:4},{value:"LinkedIn",id:"linkedin",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-3",level:4},{value:"User info parameters",id:"user-info-parameters-1",level:4},{value:"Twitter",id:"twitter",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-4",level:4},{value:"Community Providers",id:"community-providers",level:2},{value:"Common Errors",id:"common-errors",level:2},{value:"Security",id:"security",level:2},{value:"HTTPS",id:"https",level:3}];function u(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Google"}),"\n",(0,i.jsx)(n.li,{children:"Facebook"}),"\n",(0,i.jsx)(n.li,{children:"Github"}),"\n",(0,i.jsx)(n.li,{children:"Linkedin"}),"\n",(0,i.jsx)(n.li,{children:"Twitter"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["If your provider is not listed here but supports OAuth 2.0, then you can still ",(0,i.jsxs)(n.a,{href:"#custom-provider",children:["extend the ",(0,i.jsx)(n.code,{children:"AbstractProvider"})]})," class to integrate it or use a ",(0,i.jsx)(n.a,{href:"#community-providers",children:"community provider"})," below."]}),"\n",(0,i.jsx)(n.h2,{id:"get-started",children:"Get Started"}),"\n",(0,i.jsx)(n.h3,{id:"general-overview",children:"General overview"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Social auth schema",src:t(17232).A+"",width:"3872",height:"2608"})}),"\n",(0,i.jsx)(n.p,{children:"The authentication process works as follows:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["The user clicks the ",(0,i.jsx)(n.em,{children:"Log In with xxx"})," button in the browser and the client sends a request to the server."]}),"\n",(0,i.jsx)(n.li,{children:"The server redirects the user to the consent page where they are asked to give permission to log in with their account and/or give access to some of their personal information."}),"\n",(0,i.jsxs)(n.li,{children:["The user approves and the consent page redirects the user with an authorization code to the ",(0,i.jsx)(n.em,{children:"redirect"})," URI specified in the configuration."]}),"\n",(0,i.jsx)(n.li,{children:"Your application then makes one or more requests to the OAuth servers to obtain an access token and information about the user."}),"\n",(0,i.jsx)(n.li,{children:"The social provider servers return this information."}),"\n",(0,i.jsx)(n.li,{children:"Finally, your server-side application logs the user in based on this information and redirects the user when done."}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"This explanation of OAuth 2 is intentionally simplified. It highlights only the parts of the protocol that are necessary to successfully implement social authentication with Foal. But the framework also performs other tasks under the hood to fully comply with the OAuth 2.0 protocol and it adds security protection against CSRF attacks."})}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"registering-an-application",children:"Registering an application"}),"\n",(0,i.jsx)(n.p,{children:"To set up social authentication with Foal, you first need to register your application to the social provider you chose (Google, Facebook, etc). This can be done through its website."}),"\n",(0,i.jsx)(n.p,{children:"Usually your are required to provide:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["an ",(0,i.jsx)(n.em,{children:"application name"}),","]}),"\n",(0,i.jsxs)(n.li,{children:["a ",(0,i.jsx)(n.em,{children:"logo"}),","]}),"\n",(0,i.jsxs)(n.li,{children:["and ",(0,i.jsx)(n.em,{children:"redirect URIs"})," where the social provider should redirect the users once they give their consent on the provider page (ex: ",(0,i.jsx)(n.code,{children:"http://localhost:3001/signin/google/callback"}),", ",(0,i.jsx)(n.code,{children:"https://example.com/signin/google/callback"}),")."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Once done, you should receive:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["a ",(0,i.jsx)(n.em,{children:"client ID"})," that is public and identifies your application,"]}),"\n",(0,i.jsxs)(n.li,{children:["and a ",(0,i.jsx)(n.em,{children:"client secret"})," that must not be revealed and can therefore only be used on the backend side. It is used when your server communicates with the OAuth provider's servers."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["You must obtain a ",(0,i.jsx)(n.em,{children:"client secret"}),". If you do not have one, it means you probably chose the wrong option at some point."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"installation-and-configuration",children:"Installation and configuration"}),"\n",(0,i.jsx)(n.p,{children:"Once you have registered your application, install the appropriate package."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"npm install @foal/social\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Then, you will need to provide your client ID, client secret and your redirect URIs to Foal. This can be done through the usual ",(0,i.jsx)(n.a,{href:"/docs/architecture/configuration",children:"configuration files"}),"."]}),"\n",(0,i.jsxs)(s.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,i.jsx)(o.A,{value:"yaml",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"settings:\n social:\n google:\n clientId: 'xxx'\n clientSecret: 'env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)'\n redirectUri: 'http://localhost:3001/signin/google/callback'\n"})})}),(0,i.jsx)(o.A,{value:"json",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "social": {\n "google": {\n "clientId": "xxx",\n "clientSecret": "env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)",\n "redirectUri": "http://localhost:3001/signin/google/callback"\n }\n }\n }\n}\n'})})}),(0,i.jsx)(o.A,{value:"js",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n social: {\n google: {\n clientId: 'xxx',\n clientSecret: 'env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)',\n redirectUri: 'http://localhost:3001/signin/google/callback'\n }\n }\n }\n}\n"})})})]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:".env"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET=yyy\n"})}),"\n",(0,i.jsx)(n.h3,{id:"adding-controllers",children:"Adding controllers"}),"\n",(0,i.jsxs)(n.p,{children:["The last step is to add a controller that will call methods of a ",(0,i.jsx)(n.em,{children:"social service"})," to handle authentication. The example below uses Google as provider."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport { Context, dependency, Get } from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\n\nexport class AuthController {\n @dependency\n google: GoogleProvider;\n\n @Get('/signin/google')\n redirectToGoogle() {\n // Your \"Login In with Google\" button should point to this route.\n // The user will be redirected to Google auth page.\n return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true });\n }\n\n @Get('/signin/google/callback')\n async handleGoogleRedirection(ctx: Context) {\n // Once the user gives their permission to log in with Google, the OAuth server\n // will redirect the user to this route. This route must match the redirect URI.\n const { userInfo, tokens } = await this.google.getUserInfo(ctx);\n\n // Do something with the user information AND/OR the access token.\n // If you only need the access token, you can call the \"getTokens\" method.\n\n // The method usually ends with a HttpResponseRedirect object as returned value.\n }\n\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["You can also override in the ",(0,i.jsx)(n.code,{children:"redirect"})," method the scopes you want:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true, scopes: [ 'email' ] });\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Additional parameters can passed to the ",(0,i.jsx)(n.code,{children:"redirect"})," and ",(0,i.jsx)(n.code,{children:"getUserInfo"})," methods depending on the provider."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["If you want to manage the redirection on the client side manually, don't specify the ",(0,i.jsx)(n.code,{children:"isRedirection"})," option. In this case, the ",(0,i.jsx)(n.code,{children:"createHttpResponseWithConsentPageUrl"})," method returns an ",(0,i.jsx)(n.code,{children:"HttpResponseOK"})," whose body contains the URL of the consent page. The name of the body property is ",(0,i.jsx)(n.code,{children:"consentPageUrl"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"techniques",children:"Techniques"}),"\n",(0,i.jsx)(n.h3,{id:"usage-with-sessions",children:"Usage with sessions"}),"\n",(0,i.jsx)(n.p,{children:"This example shows how to manage authentication (login and registration) with sessions."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n}\n\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"auth.controller.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport {\n Context,\n dependency,\n Get,\n HttpResponseRedirect,\n Store,\n UseSessions,\n} from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\n\nimport { User } from '../entities';\n\nexport class AuthController {\n @dependency\n google: GoogleProvider;\n\n @dependency\n store: Store;\n\n @Get('/signin/google')\n redirectToGoogle() {\n return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true });\n }\n\n @Get('/signin/google/callback')\n @UseSessions({\n cookie: true,\n })\n async handleGoogleRedirection(ctx: Context) {\n const { userInfo } = await this.google.getUserInfo(ctx);\n\n if (!userInfo.email) {\n throw new Error('Google should have returned an email address.');\n }\n\n let user = await User.findOneBy({ email: userInfo.email });\n\n if (!user) {\n // If the user has not already signed up, then add them to the database.\n user = new User();\n user.email = userInfo.email;\n await user.save();\n }\n\n ctx.session!.setUser(user);\n\n return new HttpResponseRedirect('/');\n }\n\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"usage-with-jwt",children:"Usage with JWT"}),"\n",(0,i.jsx)(n.p,{children:"This example shows how to manage authentication (login and registration) with JWT."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"auth.controller.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { promisify } from 'util';\n\n// 3p\nimport {\n Context,\n dependency,\n Get,\n HttpResponseRedirect,\n} from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\nimport { getSecretOrPrivateKey, setAuthCookie } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nimport { User } from '../entities';\n\nexport class AuthController {\n @dependency\n google: GoogleProvider;\n\n @Get('/signin/google')\n redirectToGoogle() {\n return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true });\n }\n\n @Get('/signin/google/callback')\n async handleGoogleRedirection(ctx: Context) {\n const { userInfo } = await this.google.getUserInfo(ctx);\n\n if (!userInfo.email) {\n throw new Error('Google should have returned an email address.');\n }\n\n let user = await User.findOneBy({ email: userInfo.email });\n\n if (!user) {\n // If the user has not already signed up, then add them to the database.\n user = new User();\n user.email = userInfo.email;\n await user.save();\n }\n\n const payload = {\n email: user.email,\n id: user.id,\n };\n \n const jwt = await promisify(sign as any)(\n payload,\n getSecretOrPrivateKey(),\n { subject: user.id.toString() }\n );\n\n const response = new HttpResponseRedirect('/');\n await setAuthCookie(response, jwt);\n return response;\n }\n\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"custom-provider",children:"Custom Provider"}),"\n",(0,i.jsxs)(n.p,{children:["If your provider is not officially supported by Foal but supports the OAuth 2.0 protocol, you can still implement your own social service. All you need to do is to make it inherit from the ",(0,i.jsx)(n.code,{children:"AbstractProvider"})," class."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport { AbstractProvider, SocialTokens } from '@foal/core';\n\nexport interface GithubAuthParameter {\n // ...\n}\n\nexport interface GithubUserInfoParameter {\n // ...\n}\n\nexport class GithubProvider extends AbstractProvider {\n\n protected configPaths = {\n clientId: 'social.github.clientId',\n clientSecret: 'social.github.clientSecret',\n redirectUri: 'social.github.redirectUri',\n };\n\n protected authEndpoint = '...';\n protected tokenEndpoint = '...';\n protected userInfoEndpoint = '...'; // Optional. Depending on the provider.\n\n protected defaultScopes: string[] = [ 'email' ]; // Optional\n\n async getUserInfoFromTokens(tokens: SocialTokens, params?: GithubUserInfoParameter) {\n // ...\n\n // In case the server returns an error when requesting \n // user information, you can throw a UserInfoError.\n }\n\n} \n"})}),"\n",(0,i.jsx)(n.h3,{id:"sending-the-client-credentials-in-an-authorization-header",children:"Sending the Client Credentials in an Authorization Header"}),"\n",(0,i.jsxs)(n.p,{children:["When requesting the token endpoint, the provider sends the client ID and secret as a query parameter by default. If you want to send them in an ",(0,i.jsx)(n.code,{children:"Authorization"})," header using the ",(0,i.jsx)(n.em,{children:"basic"})," scheme, you can do so by setting the ",(0,i.jsx)(n.code,{children:"useAuthorizationHeaderForTokenEndpoint"})," property to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"enabling-code-flow-with-pkce",children:"Enabling Code Flow with PKCE"}),"\n",(0,i.jsxs)(n.p,{children:["If you want to enable code flow with PKCE, you can do so by setting the ",(0,i.jsx)(n.code,{children:"usePKCE"})," property to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["By default, the provider will perform a SHA256 hash to generate the code challenge. If you wish to use the plaintext code verifier string as code challenge, you can do so by setting the ",(0,i.jsx)(n.code,{children:"useCodeVerifierAsCodeChallenge"})," property to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["When using this feature, the provider encrypts the code verifier and stores it in a cookie on the client. In order to do this, you need to provide a secret using the configuration key ",(0,i.jsx)(n.code,{children:"settings.social.secret.codeVerifierSecret"}),"."]}),"\n",(0,i.jsxs)(s.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,i.jsx)(o.A,{value:"yaml",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"settings:\n social:\n secret:\n codeVerifierSecret: 'xxx'\n"})})}),(0,i.jsx)(o.A,{value:"json",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "social": {\n "secret": {\n "codeVerifierSecret": "xxx"\n }\n }\n }\n}\n'})})}),(0,i.jsx)(o.A,{value:"js",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n social: {\n secret: {\n codeVerifierSecret: 'xxx'\n }\n }\n }\n}\n"})})})]}),"\n",(0,i.jsx)(n.h2,{id:"official-providers",children:"Official Providers"}),"\n",(0,i.jsx)(n.h3,{id:"google",children:"Google"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GoogleProvider"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"openid"}),", ",(0,i.jsx)(n.code,{children:"profile"}),", ",(0,i.jsx)(n.code,{children:"email"})]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developers.google.com/identity/protocols/googlescopes",children:"Google scopes"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit the ",(0,i.jsx)(n.a,{href:"https://console.developers.google.com/apis/credentials",children:"Google API Console"})," to obtain a client ID and a client secret."]}),"\n",(0,i.jsx)(n.h4,{id:"redirection-parameters",children:"Redirection parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"createHttpResponseWithConsentPageUrl"})," method of the ",(0,i.jsx)(n.code,{children:"GoogleProvider"})," accepts additional parameters. These parameters and their description are listed ",(0,i.jsx)(n.a,{href:"https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters",children:"here"})," and are all optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"this.google.createHttpResponseWithConsentPageUrl({ /* ... */ }, {\n access_type: 'offline'\n})\n"})}),"\n",(0,i.jsx)(n.h3,{id:"facebook",children:"Facebook"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"FacebookProvider"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"email"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developers.facebook.com/docs/facebook-login/permissions/",children:"Facebook permissions"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-1",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://developers.facebook.com/",children:"Facebook's developer website"})," to create an application and obtain a client ID and a client secret."]}),"\n",(0,i.jsx)(n.h4,{id:"redirection-parameters-1",children:"Redirection parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"createHttpResponseWithConsentPageUrl"})," method of the ",(0,i.jsx)(n.code,{children:"FacebookProvider"})," accepts an additional ",(0,i.jsx)(n.code,{children:"auth_type"})," parameter which is optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"this.facebook.createHttpResponseWithConsentPageUrl({ /* ... */ }, {\n auth_type: 'rerequest'\n});\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"auth_type"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"'rerequest'"})}),(0,i.jsxs)(n.td,{children:["If a user has already declined a permission, the Facebook Login Dialog box will no longer ask for this permission. The ",(0,i.jsx)(n.code,{children:"auth_type"})," parameter explicity tells Facebook to ask the user again for the denied permission."]})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"user-info-parameters",children:"User info parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"getUserInfo"})," and ",(0,i.jsx)(n.code,{children:"getUserInfoFromTokens"})," methods of the ",(0,i.jsx)(n.code,{children:"FacebookProvider"})," accept an additional ",(0,i.jsx)(n.code,{children:"fields"})," parameter which is optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"const { userInfo } = await this.facebook.getUserInfo(ctx, {\n fields: [ 'email' ]\n})\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"fields"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string[]"})}),(0,i.jsxs)(n.td,{children:["List of fields that the returned user info object should contain. These fields may or may not be available depending on the permissions (",(0,i.jsx)(n.code,{children:"scopes"}),") that were requested with the ",(0,i.jsx)(n.code,{children:"redirect"})," method. Default: ",(0,i.jsx)(n.code,{children:"['id', 'name', 'email']"}),"."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"github",children:"Github"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GithubProvider"})}),(0,i.jsx)(n.td,{children:"none"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes",children:"Github scopes"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-2",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://github.com/settings/applications/new",children:"this page"})," to create an application and obtain a client ID and a client secret."]}),"\n",(0,i.jsxs)(n.p,{children:["Additional documentation on Github's redirect URLs can be found ",(0,i.jsx)(n.a,{href:"https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls",children:"here"}),"."]}),"\n",(0,i.jsx)(n.h4,{id:"redirection-parameters-2",children:"Redirection parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"createHttpResponseWithConsentPageUrl"})," method of the ",(0,i.jsx)(n.code,{children:"GithubProvider"})," accepts additional parameters. These parameters and their description are listed below and are all optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"this.github.createHttpResponseWithConsentPageUrl({ /* ... */ }, {\n allow_signup: false\n})\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"login"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:"Suggests a specific account to use for signing in and authorizing the app."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"allow_signup"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"boolean"})}),(0,i.jsxs)(n.td,{children:["Whether or not unauthenticated users will be offered an option to sign up for GitHub during the OAuth flow. The default is ",(0,i.jsx)(n.code,{children:"true"}),". Use ",(0,i.jsx)(n.code,{children:"false"})," in the case that a policy prohibits signups."]})]})]})]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsxs)(n.em,{children:["Source: ",(0,i.jsx)(n.a,{href:"https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#parameters",children:"https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#parameters"})]})}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"linkedin",children:"LinkedIn"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes\xa0"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"LinkedInProvider"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"r_liteprofile"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api",children:"API documentation"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-3",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://www.linkedin.com/developers/apps/new",children:"this page"})," to create an application and obtain a client ID and a client secret."]}),"\n",(0,i.jsx)(n.h4,{id:"user-info-parameters-1",children:"User info parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"getUserInfo"})," and ",(0,i.jsx)(n.code,{children:"getUserInfoFromTokens"})," methods of the ",(0,i.jsx)(n.code,{children:"LinkedInProvider"})," accept an additional ",(0,i.jsx)(n.code,{children:"projection"})," parameter which is optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"const { userInfo } = await this.linkedin.getUserInfo(ctx, {\n fields: [ 'id', 'firstName' ]\n})\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"fields"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string[]"})}),(0,i.jsxs)(n.td,{children:["List of fields that the returned user info object should contain. Additional documentation on ",(0,i.jsx)(n.a,{href:"https://developer.linkedin.com/docs/guide/v2/concepts/projections",children:"field projections"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"projection"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:"LinkedIn projection parameter."})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"twitter",children:"Twitter"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes\xa0"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TwitterProvider"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"users.read tweet.read"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me",children:"API documentation"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-4",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://developer.twitter.com/en/portal/dashboard",children:"this page"})," to create an application and obtain a client ID and a client secret. You must configure Oauth2 settings to be used with public client;"]}),"\n",(0,i.jsx)(n.h2,{id:"community-providers",children:"Community Providers"}),"\n",(0,i.jsxs)(n.p,{children:["There are no community providers available yet! If you want to share one, feel free to ",(0,i.jsx)(n.a,{href:"https://github.com/FoalTS/foal",children:"open a PR"})," on Github."]}),"\n",(0,i.jsx)(n.h2,{id:"common-errors",children:"Common Errors"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Error"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidStateError"})}),(0,i.jsxs)(n.td,{children:["The ",(0,i.jsx)(n.code,{children:"state"})," query does not match the value found in the cookie."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CodeVerifierNotFound"})}),(0,i.jsx)(n.td,{children:"The encrypted code verifier was not found in the cookie (only when using PKCE)."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AuthorizationError"})}),(0,i.jsx)(n.td,{children:"The authorization server returns an error. This can happen when a user does not give consent on the provider page."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"UserInfoError"})}),(0,i.jsxs)(n.td,{children:["Thrown in ",(0,i.jsx)(n.code,{children:"AbstractProvider.getUserFromTokens"})," if the request to the resource server is unsuccessful."]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"security",children:"Security"}),"\n",(0,i.jsx)(n.h3,{id:"https",children:"HTTPS"}),"\n",(0,i.jsx)(n.p,{children:"When deploying the application, you application must use HTTPS."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"production.yml"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"settings:\n social:\n cookie:\n # Only pass the state cookie if the request is transmitted over a secure channel (HTTPS).\n secure: true\n google:\n # Your redirect URI in production\n redirectUri: 'https://example.com/signin/google/callback'\n"})})]})}function p(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},19365:(e,n,t)=>{t.d(n,{A:()=>o});t(96540);var i=t(34164);const r={tabItem:"tabItem_Ymn6"};var s=t(74848);function o(e){let{children:n,hidden:t,className:o}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,i.A)(r.tabItem,o),hidden:t,children:n})}},11470:(e,n,t)=>{t.d(n,{A:()=>w});var i=t(96540),r=t(34164),s=t(23104),o=t(56347),a=t(205),l=t(57485),c=t(31682),d=t(89466);function h(e){return i.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,i.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:t}=e;return(0,i.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:t,attributes:i,default:r}}=e;return{value:n,label:t,attributes:i,default:r}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:t}=e;const r=(0,o.W6)(),s=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,l.aZ)(s),(0,i.useCallback)((e=>{if(!s)return;const n=new URLSearchParams(r.location.search);n.set(s,e),r.replace({...r.location,search:n.toString()})}),[s,r])]}function j(e){const{defaultValue:n,queryString:t=!1,groupId:r}=e,s=u(e),[o,l]=(0,i.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const i=t.find((e=>e.default))??t[0];if(!i)throw new Error("Unexpected error: 0 tabValues");return i.value}({defaultValue:n,tabValues:s}))),[c,h]=x({queryString:t,groupId:r}),[j,g]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[r,s]=(0,d.Dv)(t);return[r,(0,i.useCallback)((e=>{t&&s.set(e)}),[t,s])]}({groupId:r}),m=(()=>{const e=c??j;return p({value:e,tabValues:s})?e:null})();(0,a.A)((()=>{m&&l(m)}),[m]);return{selectedValue:o,selectValue:(0,i.useCallback)((e=>{if(!p({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),h(e),g(e)}),[h,g,s]),tabValues:s}}var g=t(92303);const m={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var f=t(74848);function v(e){let{className:n,block:t,selectedValue:i,selectValue:o,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,s.a_)(),d=e=>{const n=e.currentTarget,t=l.indexOf(n),r=a[t].value;r!==i&&(c(n),o(r))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=l.indexOf(e.currentTarget)+1;n=l[t]??l[0];break}case"ArrowLeft":{const t=l.indexOf(e.currentTarget)-1;n=l[t]??l[l.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":t},n),children:a.map((e=>{let{value:n,label:t,attributes:s}=e;return(0,f.jsx)("li",{role:"tab",tabIndex:i===n?0:-1,"aria-selected":i===n,ref:e=>l.push(e),onKeyDown:h,onClick:d,...s,className:(0,r.A)("tabs__item",m.tabItem,s?.className,{"tabs__item--active":i===n}),children:t??n},n)}))})}function b(e){let{lazy:n,children:t,selectedValue:r}=e;const s=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=s.find((e=>e.props.value===r));return e?(0,i.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:s.map(((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==r})))})}function y(e){const n=j(e);return(0,f.jsxs)("div",{className:(0,r.A)("tabs-container",m.tabList),children:[(0,f.jsx)(v,{...e,...n}),(0,f.jsx)(b,{...e,...n})]})}function w(e){const n=(0,g.A)();return(0,f.jsx)(y,{...e,children:h(e.children)},String(n))}},17232:(e,n,t)=>{t.d(n,{A:()=>i});const i=t.p+"assets/images/social-auth-overview-6bc9023be73ed5c7e9514909afc68f7e.png"},28453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>a});var i=t(96540);const r={},s=i.createContext(r);function o(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b03290eb.be444171.js b/assets/js/b03290eb.be444171.js deleted file mode 100644 index 93eae4fe2d..0000000000 --- a/assets/js/b03290eb.be444171.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[80],{23035:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>h});var i=t(74848),r=t(28453),s=t(11470),o=t(19365);const a={title:"Social Authentication",sidebar_label:"Social Auth"},l=void 0,c={id:"authentication/social-auth",title:"Social Authentication",description:"In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:",source:"@site/docs/authentication/social-auth.md",sourceDirName:"authentication",slug:"/authentication/social-auth",permalink:"/docs/authentication/social-auth",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/social-auth.md",tags:[],version:"current",frontMatter:{title:"Social Authentication",sidebar_label:"Social Auth"},sidebar:"someSidebar",previous:{title:"JSON Web Tokens",permalink:"/docs/authentication/jwt"},next:{title:"Administrators & Roles",permalink:"/docs/authorization/administrators-and-roles"}},d={},h=[{value:"Get Started",id:"get-started",level:2},{value:"General overview",id:"general-overview",level:3},{value:"Registering an application",id:"registering-an-application",level:3},{value:"Installation and configuration",id:"installation-and-configuration",level:3},{value:"Adding controllers",id:"adding-controllers",level:3},{value:"Techniques",id:"techniques",level:2},{value:"Usage with sessions",id:"usage-with-sessions",level:3},{value:"Usage with JWT",id:"usage-with-jwt",level:3},{value:"Custom Provider",id:"custom-provider",level:2},{value:"Sending the Client Credentials in an Authorization Header",id:"sending-the-client-credentials-in-an-authorization-header",level:3},{value:"Enabling Code Flow with PKCE",id:"enabling-code-flow-with-pkce",level:3},{value:"Official Providers",id:"official-providers",level:2},{value:"Google",id:"google",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application",level:4},{value:"Redirection parameters",id:"redirection-parameters",level:4},{value:"Facebook",id:"facebook",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-1",level:4},{value:"Redirection parameters",id:"redirection-parameters-1",level:4},{value:"User info parameters",id:"user-info-parameters",level:4},{value:"Github",id:"github",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-2",level:4},{value:"Redirection parameters",id:"redirection-parameters-2",level:4},{value:"LinkedIn",id:"linkedin",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-3",level:4},{value:"User info parameters",id:"user-info-parameters-1",level:4},{value:"Twitter",id:"twitter",level:3},{value:"Register an OAuth application",id:"register-an-oauth-application-4",level:4},{value:"Community Providers",id:"community-providers",level:2},{value:"Common Errors",id:"common-errors",level:2},{value:"Security",id:"security",level:2},{value:"HTTPS",id:"https",level:3}];function u(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"In addition to traditional password authentication, Foal provides services to authenticate users through social providers. The framework officially supports the following:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsx)(n.li,{children:"Google"}),"\n",(0,i.jsx)(n.li,{children:"Facebook"}),"\n",(0,i.jsx)(n.li,{children:"Github"}),"\n",(0,i.jsx)(n.li,{children:"Linkedin"}),"\n",(0,i.jsx)(n.li,{children:"Twitter"}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["If your provider is not listed here but supports OAuth 2.0, then you can still ",(0,i.jsxs)(n.a,{href:"#custom-provider",children:["extend the ",(0,i.jsx)(n.code,{children:"AbstractProvider"})]})," class to integrate it or use a ",(0,i.jsx)(n.a,{href:"#community-providers",children:"community provider"})," below."]}),"\n",(0,i.jsx)(n.h2,{id:"get-started",children:"Get Started"}),"\n",(0,i.jsx)(n.h3,{id:"general-overview",children:"General overview"}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.img,{alt:"Social auth schema",src:t(17232).A+"",width:"3872",height:"2608"})}),"\n",(0,i.jsx)(n.p,{children:"The authentication process works as follows:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["The user clicks the ",(0,i.jsx)(n.em,{children:"Log In with xxx"})," button in the browser and the client sends a request to the server."]}),"\n",(0,i.jsx)(n.li,{children:"The server redirects the user to the consent page where they are asked to give permission to log in with their account and/or give access to some of their personal information."}),"\n",(0,i.jsxs)(n.li,{children:["The user approves and the consent page redirects the user with an authorization code to the ",(0,i.jsx)(n.em,{children:"redirect"})," URI specified in the configuration."]}),"\n",(0,i.jsx)(n.li,{children:"Your application then makes one or more requests to the OAuth servers to obtain an access token and information about the user."}),"\n",(0,i.jsx)(n.li,{children:"The social provider servers return this information."}),"\n",(0,i.jsx)(n.li,{children:"Finally, your server-side application logs the user in based on this information and redirects the user when done."}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"This explanation of OAuth 2 is intentionally simplified. It highlights only the parts of the protocol that are necessary to successfully implement social authentication with Foal. But the framework also performs other tasks under the hood to fully comply with the OAuth 2.0 protocol and it adds security protection against CSRF attacks."})}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"registering-an-application",children:"Registering an application"}),"\n",(0,i.jsx)(n.p,{children:"To set up social authentication with Foal, you first need to register your application to the social provider you chose (Google, Facebook, etc). This can be done through its website."}),"\n",(0,i.jsx)(n.p,{children:"Usually your are required to provide:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["an ",(0,i.jsx)(n.em,{children:"application name"}),","]}),"\n",(0,i.jsxs)(n.li,{children:["a ",(0,i.jsx)(n.em,{children:"logo"}),","]}),"\n",(0,i.jsxs)(n.li,{children:["and ",(0,i.jsx)(n.em,{children:"redirect URIs"})," where the social provider should redirect the users once they give their consent on the provider page (ex: ",(0,i.jsx)(n.code,{children:"http://localhost:3001/signin/google/callback"}),", ",(0,i.jsx)(n.code,{children:"https://example.com/signin/google/callback"}),")."]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Once done, you should receive:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["a ",(0,i.jsx)(n.em,{children:"client ID"})," that is public and identifies your application,"]}),"\n",(0,i.jsxs)(n.li,{children:["and a ",(0,i.jsx)(n.em,{children:"client secret"})," that must not be revealed and can therefore only be used on the backend side. It is used when your server communicates with the OAuth provider's servers."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["You must obtain a ",(0,i.jsx)(n.em,{children:"client secret"}),". If you do not have one, it means you probably chose the wrong option at some point."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"installation-and-configuration",children:"Installation and configuration"}),"\n",(0,i.jsx)(n.p,{children:"Once you have registered your application, install the appropriate package."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"npm install @foal/social\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Then, you will need to provide your client ID, client secret and your redirect URIs to Foal. This can be done through the usual ",(0,i.jsx)(n.a,{href:"/docs/architecture/configuration",children:"configuration files"}),"."]}),"\n",(0,i.jsxs)(s.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,i.jsx)(o.A,{value:"yaml",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"settings:\n social:\n google:\n clientId: 'xxx'\n clientSecret: 'env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)'\n redirectUri: 'http://localhost:3001/signin/google/callback'\n"})})}),(0,i.jsx)(o.A,{value:"json",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "social": {\n "google": {\n "clientId": "xxx",\n "clientSecret": "env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)",\n "redirectUri": "http://localhost:3001/signin/google/callback"\n }\n }\n }\n}\n'})})}),(0,i.jsx)(o.A,{value:"js",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n social: {\n google: {\n clientId: 'xxx',\n clientSecret: 'env(SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET)',\n redirectUri: 'http://localhost:3001/signin/google/callback'\n }\n }\n }\n}\n"})})})]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:".env"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"SETTINGS_SOCIAL_GOOGLE_CLIENT_SECRET=yyy\n"})}),"\n",(0,i.jsx)(n.h3,{id:"adding-controllers",children:"Adding controllers"}),"\n",(0,i.jsxs)(n.p,{children:["The last step is to add a controller that will call methods of a ",(0,i.jsx)(n.em,{children:"social service"})," to handle authentication. The example below uses Google as provider."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport { Context, dependency, Get } from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\n\nexport class AuthController {\n @dependency\n google: GoogleProvider;\n\n @Get('/signin/google')\n redirectToGoogle() {\n // Your \"Login In with Google\" button should point to this route.\n // The user will be redirected to Google auth page.\n return this.google.redirect();\n }\n\n @Get('/signin/google/callback')\n async handleGoogleRedirection(ctx: Context) {\n // Once the user gives their permission to log in with Google, the OAuth server\n // will redirect the user to this route. This route must match the redirect URI.\n const { userInfo, tokens } = await this.google.getUserInfo(ctx);\n\n // Do something with the user information AND/OR the access token.\n // If you only need the access token, you can call the \"getTokens\" method.\n\n // The method usually ends with a HttpResponseRedirect object as returned value.\n }\n\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["You can also override in the ",(0,i.jsx)(n.code,{children:"redirect"})," method the scopes you want:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"return this.google.redirect({ scopes: [ 'email' ] });\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Additional parameters can passed to the ",(0,i.jsx)(n.code,{children:"redirect"})," and ",(0,i.jsx)(n.code,{children:"getUserInfo"})," methods depending on the provider."]}),"\n",(0,i.jsx)(n.h2,{id:"techniques",children:"Techniques"}),"\n",(0,i.jsx)(n.h3,{id:"usage-with-sessions",children:"Usage with sessions"}),"\n",(0,i.jsx)(n.p,{children:"This example shows how to manage authentication (login and registration) with sessions."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n}\n\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"auth.controller.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport {\n Context,\n dependency,\n Get,\n HttpResponseRedirect,\n Store,\n UseSessions,\n} from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\n\nimport { User } from '../entities';\n\nexport class AuthController {\n @dependency\n google: GoogleProvider;\n\n @dependency\n store: Store;\n\n @Get('/signin/google')\n redirectToGoogle() {\n return this.google.redirect();\n }\n\n @Get('/signin/google/callback')\n @UseSessions({\n cookie: true,\n })\n async handleGoogleRedirection(ctx: Context) {\n const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx);\n\n if (!userInfo.email) {\n throw new Error('Google should have returned an email address.');\n }\n\n let user = await User.findOneBy({ email: userInfo.email });\n\n if (!user) {\n // If the user has not already signed up, then add them to the database.\n user = new User();\n user.email = userInfo.email;\n await user.save();\n }\n\n ctx.session!.setUser(user);\n\n return new HttpResponseRedirect('/');\n }\n\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"usage-with-jwt",children:"Usage with JWT"}),"\n",(0,i.jsx)(n.p,{children:"This example shows how to manage authentication (login and registration) with JWT."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"user.entity.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"auth.controller.ts"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// std\nimport { promisify } from 'util';\n\n// 3p\nimport {\n Context,\n dependency,\n Get,\n HttpResponseRedirect,\n} from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\nimport { getSecretOrPrivateKey, setAuthCookie } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\n\nimport { User } from '../entities';\n\nexport class AuthController {\n @dependency\n google: GoogleProvider;\n\n @Get('/signin/google')\n redirectToGoogle() {\n return this.google.redirect();\n }\n\n @Get('/signin/google/callback')\n async handleGoogleRedirection(ctx: Context) {\n const { userInfo } = await this.google.getUserInfo<{ email: string }>(ctx);\n\n if (!userInfo.email) {\n throw new Error('Google should have returned an email address.');\n }\n\n let user = await User.findOneBy({ email: userInfo.email });\n\n if (!user) {\n // If the user has not already signed up, then add them to the database.\n user = new User();\n user.email = userInfo.email;\n await user.save();\n }\n\n const payload = {\n email: user.email,\n id: user.id,\n };\n \n const jwt = await promisify(sign as any)(\n payload,\n getSecretOrPrivateKey(),\n { subject: user.id.toString() }\n );\n\n const response = new HttpResponseRedirect('/');\n await setAuthCookie(response, jwt);\n return response;\n }\n\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"custom-provider",children:"Custom Provider"}),"\n",(0,i.jsxs)(n.p,{children:["If your provider is not officially supported by Foal but supports the OAuth 2.0 protocol, you can still implement your own social service. All you need to do is to make it inherit from the ",(0,i.jsx)(n.code,{children:"AbstractProvider"})," class."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport { AbstractProvider, SocialTokens } from '@foal/core';\n\nexport interface GithubAuthParameter {\n // ...\n}\n\nexport interface GithubUserInfoParameter {\n // ...\n}\n\nexport class GithubProvider extends AbstractProvider {\n\n protected configPaths = {\n clientId: 'social.github.clientId',\n clientSecret: 'social.github.clientSecret',\n redirectUri: 'social.github.redirectUri',\n };\n\n protected authEndpoint = '...';\n protected tokenEndpoint = '...';\n protected userInfoEndpoint = '...'; // Optional. Depending on the provider.\n\n protected defaultScopes: string[] = [ 'email' ]; // Optional\n\n async getUserInfoFromTokens(tokens: SocialTokens, params?: GithubUserInfoParameter) {\n // ...\n\n // In case the server returns an error when requesting \n // user information, you can throw a UserInfoError.\n }\n\n} \n"})}),"\n",(0,i.jsx)(n.h3,{id:"sending-the-client-credentials-in-an-authorization-header",children:"Sending the Client Credentials in an Authorization Header"}),"\n",(0,i.jsxs)(n.p,{children:["When requesting the token endpoint, the provider sends the client ID and secret as a query parameter by default. If you want to send them in an ",(0,i.jsx)(n.code,{children:"Authorization"})," header using the ",(0,i.jsx)(n.em,{children:"basic"})," scheme, you can do so by setting the ",(0,i.jsx)(n.code,{children:"useAuthorizationHeaderForTokenEndpoint"})," property to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"enabling-code-flow-with-pkce",children:"Enabling Code Flow with PKCE"}),"\n",(0,i.jsxs)(n.p,{children:["If you want to enable code flow with PKCE, you can do so by setting the ",(0,i.jsx)(n.code,{children:"usePKCE"})," property to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["By default, the provider will perform a SHA256 hash to generate the code challenge. If you wish to use the plaintext code verifier string as code challenge, you can do so by setting the ",(0,i.jsx)(n.code,{children:"useCodeVerifierAsCodeChallenge"})," property to ",(0,i.jsx)(n.code,{children:"true"}),"."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["When using this feature, the provider encrypts the code verifier and stores it in a cookie on the client. In order to do this, you need to provide a secret using the configuration key ",(0,i.jsx)(n.code,{children:"settings.social.secret.codeVerifierSecret"}),"."]}),"\n",(0,i.jsxs)(s.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,i.jsx)(o.A,{value:"yaml",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"settings:\n social:\n secret:\n codeVerifierSecret: 'xxx'\n"})})}),(0,i.jsx)(o.A,{value:"json",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "social": {\n "secret": {\n "codeVerifierSecret": "xxx"\n }\n }\n }\n}\n'})})}),(0,i.jsx)(o.A,{value:"js",children:(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n social: {\n secret: {\n codeVerifierSecret: 'xxx'\n }\n }\n }\n}\n"})})})]}),"\n",(0,i.jsx)(n.h2,{id:"official-providers",children:"Official Providers"}),"\n",(0,i.jsx)(n.h3,{id:"google",children:"Google"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GoogleProvider"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"openid"}),", ",(0,i.jsx)(n.code,{children:"profile"}),", ",(0,i.jsx)(n.code,{children:"email"})]}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developers.google.com/identity/protocols/googlescopes",children:"Google scopes"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit the ",(0,i.jsx)(n.a,{href:"https://console.developers.google.com/apis/credentials",children:"Google API Console"})," to obtain a client ID and a client secret."]}),"\n",(0,i.jsx)(n.h4,{id:"redirection-parameters",children:"Redirection parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"redirect"})," method of the ",(0,i.jsx)(n.code,{children:"GoogleProvider"})," accepts additional parameters. These parameters and their description are listed ",(0,i.jsx)(n.a,{href:"https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters",children:"here"})," and are all optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"this.google.redirect({ /* ... */ }, {\n access_type: 'offline'\n})\n"})}),"\n",(0,i.jsx)(n.h3,{id:"facebook",children:"Facebook"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"FacebookProvider"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"email"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developers.facebook.com/docs/facebook-login/permissions/",children:"Facebook permissions"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-1",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://developers.facebook.com/",children:"Facebook's developer website"})," to create an application and obtain a client ID and a client secret."]}),"\n",(0,i.jsx)(n.h4,{id:"redirection-parameters-1",children:"Redirection parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"redirect"})," method of the ",(0,i.jsx)(n.code,{children:"FacebookProvider"})," accepts an additional ",(0,i.jsx)(n.code,{children:"auth_type"})," parameter which is optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"this.facebook.redirect({ /* ... */ }, {\n auth_type: 'rerequest'\n});\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"auth_type"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"'rerequest'"})}),(0,i.jsxs)(n.td,{children:["If a user has already declined a permission, the Facebook Login Dialog box will no longer ask for this permission. The ",(0,i.jsx)(n.code,{children:"auth_type"})," parameter explicity tells Facebook to ask the user again for the denied permission."]})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"user-info-parameters",children:"User info parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"getUserInfo"})," and ",(0,i.jsx)(n.code,{children:"getUserInfoFromTokens"})," methods of the ",(0,i.jsx)(n.code,{children:"FacebookProvider"})," accept an additional ",(0,i.jsx)(n.code,{children:"fields"})," parameter which is optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"const { userInfo } = await this.facebook.getUserInfo(ctx, {\n fields: [ 'email' ]\n})\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"fields"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string[]"})}),(0,i.jsxs)(n.td,{children:["List of fields that the returned user info object should contain. These fields may or may not be available depending on the permissions (",(0,i.jsx)(n.code,{children:"scopes"}),") that were requested with the ",(0,i.jsx)(n.code,{children:"redirect"})," method. Default: ",(0,i.jsx)(n.code,{children:"['id', 'name', 'email']"}),"."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"github",children:"Github"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"GithubProvider"})}),(0,i.jsx)(n.td,{children:"none"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/#available-scopes",children:"Github scopes"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-2",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://github.com/settings/applications/new",children:"this page"})," to create an application and obtain a client ID and a client secret."]}),"\n",(0,i.jsxs)(n.p,{children:["Additional documentation on Github's redirect URLs can be found ",(0,i.jsx)(n.a,{href:"https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#redirect-urls",children:"here"}),"."]}),"\n",(0,i.jsx)(n.h4,{id:"redirection-parameters-2",children:"Redirection parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"redirect"})," method of the ",(0,i.jsx)(n.code,{children:"GithubProvider"})," accepts additional parameters. These parameters and their description are listed below and are all optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"this.github.redirect({ /* ... */ }, {\n allow_signup: false\n})\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"login"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:"Suggests a specific account to use for signing in and authorizing the app."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"allow_signup"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"boolean"})}),(0,i.jsxs)(n.td,{children:["Whether or not unauthenticated users will be offered an option to sign up for GitHub during the OAuth flow. The default is ",(0,i.jsx)(n.code,{children:"true"}),". Use ",(0,i.jsx)(n.code,{children:"false"})," in the case that a policy prohibits signups."]})]})]})]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsxs)(n.em,{children:["Source: ",(0,i.jsx)(n.a,{href:"https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#parameters",children:"https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/#parameters"})]})}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"linkedin",children:"LinkedIn"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes\xa0"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"LinkedInProvider"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"r_liteprofile"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api",children:"API documentation"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-3",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://www.linkedin.com/developers/apps/new",children:"this page"})," to create an application and obtain a client ID and a client secret."]}),"\n",(0,i.jsx)(n.h4,{id:"user-info-parameters-1",children:"User info parameters"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"getUserInfo"})," and ",(0,i.jsx)(n.code,{children:"getUserInfoFromTokens"})," methods of the ",(0,i.jsx)(n.code,{children:"LinkedInProvider"})," accept an additional ",(0,i.jsx)(n.code,{children:"projection"})," parameter which is optional."]}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Example"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-typescript",children:"const { userInfo } = await this.linkedin.getUserInfo(ctx, {\n fields: [ 'id', 'firstName' ]\n})\n"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Name"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"fields"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string[]"})}),(0,i.jsxs)(n.td,{children:["List of fields that the returned user info object should contain. Additional documentation on ",(0,i.jsx)(n.a,{href:"https://developer.linkedin.com/docs/guide/v2/concepts/projections",children:"field projections"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"projection"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"string"})}),(0,i.jsx)(n.td,{children:"LinkedIn projection parameter."})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"twitter",children:"Twitter"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service name"}),(0,i.jsx)(n.th,{children:"Default scopes"}),(0,i.jsx)(n.th,{children:"Available scopes\xa0"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TwitterProvider"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"users.read tweet.read"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.a,{href:"https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me",children:"API documentation"})})]})})]}),"\n",(0,i.jsx)(n.h4,{id:"register-an-oauth-application-4",children:"Register an OAuth application"}),"\n",(0,i.jsxs)(n.p,{children:["Visit ",(0,i.jsx)(n.a,{href:"https://developer.twitter.com/en/portal/dashboard",children:"this page"})," to create an application and obtain a client ID and a client secret. You must configure Oauth2 settings to be used with public client;"]}),"\n",(0,i.jsx)(n.h2,{id:"community-providers",children:"Community Providers"}),"\n",(0,i.jsxs)(n.p,{children:["There are no community providers available yet! If you want to share one, feel free to ",(0,i.jsx)(n.a,{href:"https://github.com/FoalTS/foal",children:"open a PR"})," on Github."]}),"\n",(0,i.jsx)(n.h2,{id:"common-errors",children:"Common Errors"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Error"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidStateError"})}),(0,i.jsxs)(n.td,{children:["The ",(0,i.jsx)(n.code,{children:"state"})," query does not match the value found in the cookie."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CodeVerifierNotFound"})}),(0,i.jsx)(n.td,{children:"The encrypted code verifier was not found in the cookie (only when using PKCE)."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AuthorizationError"})}),(0,i.jsx)(n.td,{children:"The authorization server returns an error. This can happen when a user does not give consent on the provider page."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"UserInfoError"})}),(0,i.jsxs)(n.td,{children:["Thrown in ",(0,i.jsx)(n.code,{children:"AbstractProvider.getUserFromTokens"})," if the request to the resource server is unsuccessful."]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"security",children:"Security"}),"\n",(0,i.jsx)(n.h3,{id:"https",children:"HTTPS"}),"\n",(0,i.jsx)(n.p,{children:"When deploying the application, you application must use HTTPS."}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"production.yml"})}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",children:"settings:\n social:\n cookie:\n # Only pass the state cookie if the request is transmitted over a secure channel (HTTPS).\n secure: true\n google:\n # Your redirect URI in production\n redirectUri: 'https://example.com/signin/google/callback'\n"})})]})}function p(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},19365:(e,n,t)=>{t.d(n,{A:()=>o});t(96540);var i=t(34164);const r={tabItem:"tabItem_Ymn6"};var s=t(74848);function o(e){let{children:n,hidden:t,className:o}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,i.A)(r.tabItem,o),hidden:t,children:n})}},11470:(e,n,t)=>{t.d(n,{A:()=>w});var i=t(96540),r=t(34164),s=t(23104),o=t(56347),a=t(205),l=t(57485),c=t(31682),d=t(89466);function h(e){return i.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,i.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:t}=e;return(0,i.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:t,attributes:i,default:r}}=e;return{value:n,label:t,attributes:i,default:r}}))}(t);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,t])}function p(e){let{value:n,tabValues:t}=e;return t.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:t}=e;const r=(0,o.W6)(),s=function(e){let{queryString:n=!1,groupId:t}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!t)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return t??null}({queryString:n,groupId:t});return[(0,l.aZ)(s),(0,i.useCallback)((e=>{if(!s)return;const n=new URLSearchParams(r.location.search);n.set(s,e),r.replace({...r.location,search:n.toString()})}),[s,r])]}function j(e){const{defaultValue:n,queryString:t=!1,groupId:r}=e,s=u(e),[o,l]=(0,i.useState)((()=>function(e){let{defaultValue:n,tabValues:t}=e;if(0===t.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:t}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${t.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const i=t.find((e=>e.default))??t[0];if(!i)throw new Error("Unexpected error: 0 tabValues");return i.value}({defaultValue:n,tabValues:s}))),[c,h]=x({queryString:t,groupId:r}),[j,m]=function(e){let{groupId:n}=e;const t=function(e){return e?`docusaurus.tab.${e}`:null}(n),[r,s]=(0,d.Dv)(t);return[r,(0,i.useCallback)((e=>{t&&s.set(e)}),[t,s])]}({groupId:r}),g=(()=>{const e=c??j;return p({value:e,tabValues:s})?e:null})();(0,a.A)((()=>{g&&l(g)}),[g]);return{selectedValue:o,selectValue:(0,i.useCallback)((e=>{if(!p({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),h(e),m(e)}),[h,m,s]),tabValues:s}}var m=t(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var f=t(74848);function v(e){let{className:n,block:t,selectedValue:i,selectValue:o,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,s.a_)(),d=e=>{const n=e.currentTarget,t=l.indexOf(n),r=a[t].value;r!==i&&(c(n),o(r))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const t=l.indexOf(e.currentTarget)+1;n=l[t]??l[0];break}case"ArrowLeft":{const t=l.indexOf(e.currentTarget)-1;n=l[t]??l[l.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":t},n),children:a.map((e=>{let{value:n,label:t,attributes:s}=e;return(0,f.jsx)("li",{role:"tab",tabIndex:i===n?0:-1,"aria-selected":i===n,ref:e=>l.push(e),onKeyDown:h,onClick:d,...s,className:(0,r.A)("tabs__item",g.tabItem,s?.className,{"tabs__item--active":i===n}),children:t??n},n)}))})}function b(e){let{lazy:n,children:t,selectedValue:r}=e;const s=(Array.isArray(t)?t:[t]).filter(Boolean);if(n){const e=s.find((e=>e.props.value===r));return e?(0,i.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:s.map(((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==r})))})}function y(e){const n=j(e);return(0,f.jsxs)("div",{className:(0,r.A)("tabs-container",g.tabList),children:[(0,f.jsx)(v,{...e,...n}),(0,f.jsx)(b,{...e,...n})]})}function w(e){const n=(0,m.A)();return(0,f.jsx)(y,{...e,children:h(e.children)},String(n))}},17232:(e,n,t)=>{t.d(n,{A:()=>i});const i=t.p+"assets/images/social-auth-overview-6bc9023be73ed5c7e9514909afc68f7e.png"},28453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>a});var i=t(96540);const r={},s=i.createContext(r);function o(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b2b675dd.458979b2.js b/assets/js/b2b675dd.705a6984.js similarity index 75% rename from assets/js/b2b675dd.458979b2.js rename to assets/js/b2b675dd.705a6984.js index b2735445eb..df15e1d630 100644 --- a/assets/js/b2b675dd.458979b2.js +++ b/assets/js/b2b675dd.705a6984.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1991],{29775:e=>{e.exports=JSON.parse('{"permalink":"/blog","page":1,"postsPerPage":10,"totalPages":3,"totalCount":25,"nextPage":"/blog/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1991],{29775:e=>{e.exports=JSON.parse('{"permalink":"/blog","page":1,"postsPerPage":10,"totalPages":3,"totalCount":26,"nextPage":"/blog/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/assets/js/b2f554cd.186b7ca5.js b/assets/js/b2f554cd.186b7ca5.js new file mode 100644 index 0000000000..dbd3689c97 --- /dev/null +++ b/assets/js/b2f554cd.186b7ca5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5894],{76042:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"/2024/08/22/version-4.5-release-notes","metadata":{"permalink":"/blog/2024/08/22/version-4.5-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-08-22-version-4.5-release-notes.md","source":"@site/blog/2024-08-22-version-4.5-release-notes.md","title":"Version 4.5 release notes","description":"Banner","date":"2024-08-22T00:00:00.000Z","formattedDate":"August 22, 2024","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":2.425,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.5 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.5-release-notes.png","tags":["release"]},"unlisted":false,"nextItem":{"title":"Version 4.4 release notes","permalink":"/blog/2024/04/25/version-4.4-release-notes"}},"content":"![Banner](./assets/version-4.5-is-here/banner.png)\\n\\nVersion 4.5 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Asynchronous tasks\\n\\nIn some situations, we need to execute a specific task without waiting for it and without blocking the request.\\n\\nThis could be, for example, sending a specific message to the CRM or company chat. In this case, the user needs to be able to see his or her request completed as quickly as possible, even if the request to the CRM takes some time or fails.\\n\\nTo this end, Foal version 4.5 provides an `AsyncService` to execute these tasks asynchronously, and correctly catch and log their errors where appropriate.\\n\\n```typescript\\nimport { AsyncService, dependency } from \'@foal/core\';\\n\\nimport { CRMService } from \'./somewhere\';\\n\\nexport class SubscriptionService {\\n @dependency\\n asyncService: AsyncService;\\n\\n @dependency\\n crmService: CRMService;\\n\\n async subscribe(userId: number): Promise {\\n // Do something\\n\\n this.asyncService.run(() => this.crmService.updateUser(userId));\\n }\\n}\\n\\n```\\n\\n## Social authentication for SPAs\\n\\nIf you wish to manually manage the redirection to the consent page on the client side (which is often necessary when developing an SPA), you can now do so with the `createHttpResponseWithConsentPageUrl` method. It returns an `HttpResponseOK` whose body contains the URL of the consent page.\\n\\n```typescript\\nexport class AuthController {\\n @dependency\\n google: GoogleProvider;\\n\\n @Get(\'/signin/google\')\\n getConsentPageURL() {\\n return this.google.createHttpResponseWithConsentPageUrl();\\n }\\n \\n // ...\\n\\n}\\n\\n```\\n\\n## Google social authentification\\n\\nThe typing of the `GoogleProvider` service has been improved. The `userInfo` property returned by `getUserInfo` is now typed with the values returned by the Google server.\\n\\n```typescript\\nconst { userInfo } = await this.googleProvider.getUserInfo(...);\\n\\n// userInfo.email, userInfo.family_name, etc\\n```\\n\\n## Logging improvements\\n\\nIn previous versions, the util function `displayServerURL` and configuration errors printed logs on several lines, which was not appropriate for logging software.\\n\\nFrom version 4.5 onwards, configuration errors are displayed on a single line and the `displayServerURL` function is marked as deprecated.\\n\\n## CLI fixes\\n\\nWhen running `npx foal connect react` to connect the React application to the Foal application in development, the following features did not work:\\n- Proxify requests from the client to the server without needing to enable CORS or specify a different port in development.\\n- Build the client application in the server application\'s public directory.\\n\\nThis is fixed in v4.5.\\n\\n## Global use of CLI deprecated\\n\\nIn previous versions, the tutorial suggested installing the CLI globally to create a new application or generate files. However, it is considered bad practice to install a dependency globally for local use.\\n\\nIn addition, the CLI was also installed locally so that the build command would work when deploying the application to a CI or to production. This was maintaining two versions of the CLI.\\n\\nTo correct this, in the documentation and examples, the CLI is now always installed and used locally. To use it, simply add `npx` before each command (except for `createapp`).\\n\\n```bash\\n# Before\\nfoal createapp my-app\\n\\nfoal generate script foobar\\nfoal createsecret\\n\\n# After\\nnpx @foal/cli createapp my-app\\n\\nnpx foal generate script foobar\\nnpx foal createsecret\\n```"},{"id":"/2024/04/25/version-4.4-release-notes","metadata":{"permalink":"/blog/2024/04/25/version-4.4-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-25-version-4.4-release-notes.md","source":"@site/blog/2024-04-25-version-4.4-release-notes.md","title":"Version 4.4 release notes","description":"Banner","date":"2024-04-25T00:00:00.000Z","formattedDate":"April 25, 2024","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.19,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.4 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.4-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.5 release notes","permalink":"/blog/2024/08/22/version-4.5-release-notes"},"nextItem":{"title":"Version 4.3 release notes","permalink":"/blog/2024/04/16/version-4.3-release-notes"}},"content":"![Banner](./assets/version-4.4-is-here/banner.png)\\n\\nVersion 4.4 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\nThis release updates Foal\'s sub-dependencies, including the `express` library, which presents a moderate vulnerability in versions prior to 4.19.2.\\n\\nThanks to [Lucho](https://github.com/lcnvdl) for reporting this vulnerability in the first place!"},{"id":"/2024/04/16/version-4.3-release-notes","metadata":{"permalink":"/blog/2024/04/16/version-4.3-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-16-version-4.3-release-notes.md","source":"@site/blog/2024-04-16-version-4.3-release-notes.md","title":"Version 4.3 release notes","description":"Banner","date":"2024-04-16T00:00:00.000Z","formattedDate":"April 16, 2024","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.125,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.3 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.3-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.4 release notes","permalink":"/blog/2024/04/25/version-4.4-release-notes"},"nextItem":{"title":"Version 4.2 release notes","permalink":"/blog/2023/10/29/version-4.2-release-notes"}},"content":"![Banner](./assets/version-4.3-is-here/banner.png)\\n\\nVersion 4.3 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better CLI ouput when script arguments are invalid\\n\\nPreviously, when executing `foal run my-script` with invalid arguments, the CLI would only display one error at a time.\\n\\nFor example, with the following schema and arguments, we would only get this error message:\\n\\n```typescript\\nexport const schema = {\\n type: \'object\', \\n properties: {\\n email: { type: \'string\', format: \'email\', maxLength: 2 },\\n password: { type: \'string\' },\\n n: { type: \'number\', maximum: 10 }\\n },\\n required: [\'password\']\\n};\\n```\\n\\n```bash\\nfoal run my-script email=bar n=11\\n```\\n\\n```\\nError: The command line arguments must match format \\"email\\".\\n```\\n\\nFrom version 4.3 onwards, the CLI logs all validation errors and with a more meaningful description.\\n\\n```\\nScript error: arguments must have required property \'password\'.\\nScript error: the value of \\"email\\" must NOT have more than 2 characters.\\nScript error: the value of \\"email\\" must match format \\"email\\".\\nScript error: the value of \\"n\\" must be <= 10.\\n```\\n\\n## [Fix] the logger no longer throws an error in development when the client request is interrupted\\n\\nUsing the logger\'s `dev` format, Foal would occasionally throw the error `TypeError: Cannot read properties of null`.\\n\\nThis would occur when the connection with the client was lost, which happens, for example, when the React client server hotly reloads.\\n\\nThis version fixes this error."},{"id":"/2023/10/29/version-4.2-release-notes","metadata":{"permalink":"/blog/2023/10/29/version-4.2-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-10-29-version-4.2-release-notes.md","source":"@site/blog/2023-10-29-version-4.2-release-notes.md","title":"Version 4.2 release notes","description":"Banner","date":"2023-10-29T00:00:00.000Z","formattedDate":"October 29, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.615,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.2 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.2-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.3 release notes","permalink":"/blog/2024/04/16/version-4.3-release-notes"},"nextItem":{"title":"Version 4.1 release notes","permalink":"/blog/2023/10/24/version-4.1-release-notes"}},"content":"![Banner](./assets/version-4.2-is-here/banner.png)\\n\\nVersion 4.2 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better logging for socket.io controllers\\n\\nSocket.io messages are now logged in the same way as HTTP requests.\\n\\n## AJV strict mode can be disabled\\n\\nAJV [strict mode](https://ajv.js.org/strict-mode.html) can be disabled thanks to the new config key `settings.ajv.strict`:\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"ajv\\": {\\n \\"strict\\": false\\n }\\n }\\n}\\n```\\n\\n## `foal connect angular` command fixed\\n\\nThe command that allows to set up a project with Angular and Foal has been fixed to support the latest versions of Angular. \\n\\n## Cache control can be disabled for static files\\n\\nThe `cacheControl` option of the `express.static` middleware can be passed through the configuration.\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"staticFiles\\": {\\n \\"cacheControl\\": false\\n }\\n }\\n}\\n```"},{"id":"/2023/10/24/version-4.1-release-notes","metadata":{"permalink":"/blog/2023/10/24/version-4.1-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-10-24-version-4.1-release-notes.md","source":"@site/blog/2023-10-24-version-4.1-release-notes.md","title":"Version 4.1 release notes","description":"Banner","date":"2023-10-24T00:00:00.000Z","formattedDate":"October 24, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.625,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.1 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.1-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.2 release notes","permalink":"/blog/2023/10/29/version-4.2-release-notes"},"nextItem":{"title":"Version 4.0 release notes","permalink":"/blog/2023/09/11/version-4.0-release-notes"}},"content":"![Banner](./assets/version-4.1-is-here/banner.png)\\n\\nVersion 4.1 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better logging\\n\\nFoal now features a true logging system. Full documentation can be found [here](/docs/common/logging).\\n\\n### New recommended configuration\\n\\nIt is recommended to switch to this configuration to take full advantage of the new logging system.\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"loggerFormat\\": \\"foal\\"\\n }\\n}\\n```\\n\\n*config/development.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"logger\\": {\\n \\"format\\": \\"dev\\"\\n }\\n }\\n}\\n```\\n\\n## Request IDs\\n\\nOn each request, a request ID is now generated randomly. It can be read through `ctx.request.id`.\\n\\nIf the `X-Request-ID` header exists in the request, then the header value is used as the request identifier.\\n\\n## XML requests\\n\\nIf a request is sent with the `application/xml` header, the XML content is now available under `ctx.request.body`."},{"id":"/2023/09/11/version-4.0-release-notes","metadata":{"permalink":"/blog/2023/09/11/version-4.0-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-09-11-version-4.0-release-notes.md","source":"@site/blog/2023-09-11-version-4.0-release-notes.md","title":"Version 4.0 release notes","description":"Banner","date":"2023-09-11T00:00:00.000Z","formattedDate":"September 11, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.005,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.0 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.0-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.1 release notes","permalink":"/blog/2023/10/24/version-4.1-release-notes"},"nextItem":{"title":"Version 3.3 release notes","permalink":"/blog/2023/08/13/version-3.3-release-notes"}},"content":"![Banner](./assets/version-4.0-is-here/banner.png)\\n\\nVersion 4.0 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Description\\n\\nThe goals of this major release are to\\n- support the latest active versions of Node (18 and 20) and update all internal dependencies to their latest major versions\\n- facilitate framework maintenance.\\n\\nFull details can be found [here](https://github.com/FoalTS/foal/issues/1223).\\n\\n## Migration guide\\n\\n- Run `npx foal upgrade`.\\n- Version 16 of Node is not supported anymore. Upgrade to version 18 or version 20.\\n- Support of MariaDB has been dropped.\\n- If you use any of these dependencies, upgrade `typeorm` to v0.3.17, `graphql` to v16, `type-graphql` to v2, `class-validator` to v0.14, `mongodb` to v5 and `@socket.io/redis-adapter` to v8.\\n- If you use both TypeORM and `MongoDBStore`, there is no need anymore to maintain two versions of `mongodb`. You can use version 5 of `mongodb` dependency.\\n- If you use `@foal/socket.io` with redis, install `socket.io-adapter`.\\n- Support for `better-sqlite` driver has been dropped. Use the `sqlite3` driver instead. In DB configuration, use `type: \'sqlite\'` instead of `type: \'better-sqlite3\'`.\\n- In your project dependencies, upgrade `@types/node` to v18.11.9.\\n- If you use TypeORM with MongoDB, for the entities definitions, rename `import { ObjectID } from \'typeorm\';` to `import { ObjectId } from \'typeorm\';`"},{"id":"/2023/08/13/version-3.3-release-notes","metadata":{"permalink":"/blog/2023/08/13/version-3.3-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-08-13-version-3.3-release-notes.md","source":"@site/blog/2023-08-13-version-3.3-release-notes.md","title":"Version 3.3 release notes","description":"Banner","date":"2023-08-13T00:00:00.000Z","formattedDate":"August 13, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.265,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.3 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.3-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.0 release notes","permalink":"/blog/2023/09/11/version-4.0-release-notes"},"nextItem":{"title":"Version 3.2 release notes","permalink":"/blog/2023/04/04/version-3.2-release-notes"}},"content":"![Banner](./assets/version-3.3-is-here/banner.png)\\n\\nVersion 3.3 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better security for JWT\\n\\nThe `jsonwebtoken` dependency has been upgraded to v9 to address [security issues](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md#900---2022-12-21).\\n\\n> Note that RSA key size now must be 2048 bits or greater. Make sure to check the size of your RSA key before upgrading to this version."},{"id":"/2023/04/04/version-3.2-release-notes","metadata":{"permalink":"/blog/2023/04/04/version-3.2-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-04-04-version-3.2-release-notes.md","source":"@site/blog/2023-04-04-version-3.2-release-notes.md","title":"Version 3.2 release notes","description":"Banner","date":"2023-04-04T00:00:00.000Z","formattedDate":"April 4, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.54,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.2 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.2-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.3 release notes","permalink":"/blog/2023/08/13/version-3.3-release-notes"},"nextItem":{"title":"Version 3.1 release notes","permalink":"/blog/2022/11/28/version-3.1-release-notes"}},"content":"![Banner](./assets/version-3.2-is-here/banner.png)\\n\\nVersion 3.2 of [Foal](https://foalts.org/) is out! Here are the improvements that it brings:\\n\\n\x3c!--truncate--\x3e\\n\\n## New package `@foal/password`\\n\\nThe `foal/password` package, which was removed in v3.0, has been re-added. It offers an `isCommon` method to check if a password is too common:\\n\\n```typescript\\nconst isPasswordTooCommon = await isCommon(password);\\n```\\n\\n## Read the controller and the controller method names in request contexts\\n\\nThe `Context` and `WebsocketContext` have two new properties:\\n\\n\\n | Name | Type | Description |\\n | --- | --- | --- |\\n | `controllerName` | `string` | The name of the controller class. |\\n | `controllerMethodName` | `string` | The name of the controller method. |"},{"id":"/2022/11/28/version-3.1-release-notes","metadata":{"permalink":"/blog/2022/11/28/version-3.1-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-11-28-version-3.1-release-notes.md","source":"@site/blog/2022-11-28-version-3.1-release-notes.md","title":"Version 3.1 release notes","description":"Banner","date":"2022-11-28T00:00:00.000Z","formattedDate":"November 28, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.765,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.1 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.1-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.2 release notes","permalink":"/blog/2023/04/04/version-3.2-release-notes"},"nextItem":{"title":"Version 3.0 release notes","permalink":"/blog/2022/11/01/version-3.0-release-notes"}},"content":"![Banner](./assets/version-3.1-is-here/banner.png)\\n\\nVersion 3.1 of [Foal](https://foalts.org/) is out! Here are the improvements that it brings:\\n\\n\x3c!--truncate--\x3e\\n\\n## New `foal upgrade` command\\n\\nThis command allows you to upgrade all `@foal/*` dependencies and dev dependencies to a given version.\\n\\n*Examples*\\n```bash\\nfoal upgrade # upgrade to the latest version\\nfoal upgrade 3.0.0\\nfoal upgrade \\"~3.0.0\\"\\n```\\n\\n## Social authentication supports subdomains\\n\\nIf you\'re using multiple subdomains domains to handle social authentication, you can now do so by specifying a custom cookie domain in the configuration:\\n\\n```yaml\\nsettings:\\n social:\\n cookie:\\n domain: foalts.org\\n```\\n\\n## Regression on OpenAPI keyword \\"example\\" has been fixed\\n\\nIn version 3.0, using the keyword `example` in an validation object was raising an error. This has been fixed.\\n\\n## `.env` files support whitespaces\\n\\nWhitespaces around the equal symbol are now allowed:\\n\\n```bash\\nFOO_BAR_WITH_WHITESPACES_AROUND_THE_NAME = hello you\\n```\\n\\n## Value of the `Strict-Transport-Security` header has been increased\\n\\nIt has been increased from 15,552,000 to 31,536,000."},{"id":"/2022/11/01/version-3.0-release-notes","metadata":{"permalink":"/blog/2022/11/01/version-3.0-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-11-01-version-3.0-release-notes.md","source":"@site/blog/2022-11-01-version-3.0-release-notes.md","title":"Version 3.0 release notes","description":"Banner","date":"2022-11-01T00:00:00.000Z","formattedDate":"November 1, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":6.72,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.0 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.0-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.1 release notes","permalink":"/blog/2022/11/28/version-3.1-release-notes"},"nextItem":{"title":"Version 2.11 release notes","permalink":"/blog/2022/10/09/version-2.11-release-notes"}},"content":"![Banner](./assets/version-3.0-is-here/banner.png)\\n\\nVersion 3.0 of [Foal](https://foalts.org/) is finally there!\\n\\nIt\'s been a long work and I\'m excited to share with you the new features of the framework \ud83c\udf89 . The upgrading guide can be found [here](https://foalts.org/docs/3.x/upgrade-to-v3/).\\n\\nHere are the new features and improvements of version 3!\\n\\n\x3c!--truncate--\x3e\\n\\n## Full support of TypeORM v0.3\\n\\n> For those new to Foal, TypeORM is the default ORM used in all new projects. But you can use any other ORM or query builder if you want, as the core framework is ORM independent.\\n\\nTypeORM v0.3 provides greater typing safety and this is something that will be appreciated when moving to the new version of Foal.\\n\\nThe version 0.3 of TypeORM has a lot of changes compared to the version 0.2 though. Features such as the `ormconfig.json` file have been removed and functions such as `createConnection`, `getManager` or `getRepository` have been deprecated.\\n\\nA lot of work has been done to make sure that `@foal/typeorm`, new projects generated by the CLI and examples in the documentation use version 0.3 of TypeORM without relying on deprecated functions or patterns.\\n\\nIn particular, the connection to the database is now managed by a file `src/db.ts` that replaces the older `ormconfig.json`.\\n\\n## Code simplified\\n\\nSome parts of the framework have been simplified to require less code and make it more understandable.\\n\\n### Authentication\\n\\nThe `@UseSessions` and `@JWTRequired` authentication hooks called obscure functions such as `fetchUser`, `fetchUserWithPermissions` to populate the `ctx.user` property. The real role of these functions was not clear and a newcomer to the framework could wonder what they were for.\\n\\nThis is why these functions have been removed and replaced by direct calls to database models.\\n\\n```typescript\\n// Version 2\\n@UseSessions({ user: fetchUser(User) })\\n@JWTRequired({ user: fetchUserWithPermissions(User) })\\n\\n// Version 3\\n@UseSessions({ user: (id: number) => User.findOneBy({ id }) })\\n@JWTRequired({ user: (id: number) => User.findOneWithPermissionsBy({ id }) })\\n```\\n\\n### File upload\\n\\nWhen uploading files in a _multipart/form-data_ request, it was not allowed to pass optional fields. This is now possible. \\n\\nThe interface of the `@ValidateMultipartFormDataBody` hook, renamed to `@ParseAndValidateFiles` to be more understandable for people who don\'t know the HTTP protocol handling the upload, has been simplified.\\n\\n*Examples with only files*\\n```typescript\\n// Version 2\\n@ValidateMultipartFormDataBody({\\n files: {\\n profile: { required: true }\\n }\\n})\\n\\n// Version 3\\n@ParseAndValidateFiles({\\n profile: { required: true }\\n})\\n```\\n\\n*Examples with files and fields*\\n```typescript\\n// Version 2\\n@ValidateMultipartFormDataBody({\\n files: {\\n profile: { required: true }\\n }\\n fields: {\\n description: { type: \'string\' }\\n }\\n})\\n\\n// Version 3\\n@ParseAndValidateFiles(\\n {\\n profile: { required: true }\\n },\\n // The second parameter is optional\\n // and is used to add fields. It expects an AJV object.\\n {\\n type: \'object\',\\n properties: {\\n description: { type: \'string\' }\\n },\\n required: [\'description\'],\\n additionalProperties: false\\n }\\n)\\n```\\n\\n### Database models\\n\\nUsing functions like `getRepository` or `getManager` to manipulate data in a database is not necessarily obvious to newcomers. It adds complexity that is not necessary for small or medium sized projects. Most frameworks prefer to use the Active Record pattern for simplicity.\\n\\nThis is why, from version 3 and to take into account that TypeORM v0.3 no longer uses a global connection, the examples in the documentation and the generators will extend all the models from `BaseEntity`. Of course, it will still be possible to use the functions below if desired. \\n\\n```typescript\\n// Version 2\\n@Entity()\\nclass User {}\\n\\nconst user = getRepository(User).create();\\nawait getRepository(User).save(user);\\n\\n// Version 3\\n@Entity()\\nclass User extends BaseEntity {}\\n\\nconst user = new User();\\nawait user.save();\\n```\\n\\n## Better typing\\n\\nThe use of TypeScript types has been improved and some parts of the framework ensure better type safety.\\n\\n### Validation with AJV\\n\\nFoal\'s version uses `ajv@8` which allows you to bind a TypeScript type with a JSON schema object. To do this, you can import the generic type `JSONSchemaType` to build the interface of the schema object.\\n\\n```typescript\\nimport { JSONSchemaType } from \'ajv\';\\n\\ninterface MyData {\\n foo: number;\\n bar?: string\\n}\\n\\nconst schema: JSONSchemaType = {\\n type: \'object\',\\n properties: {\\n foo: { type: \'integer\' },\\n bar: { type: \'string\', nullable: true }\\n },\\n required: [\'foo\'],\\n additionalProperties: false\\n}\\n```\\n\\n### File upload\\n\\nIn version 2, handling file uploads in the controller was tedious because all types were `any`. Starting with version 3, it is no longer necessary to cast the types to `File` or `File[]`:\\n\\n```typescript\\n// Version 2\\nconst name = ctx.request.body.fields.name;\\nconst file = ctx.request.body.files.avatar as File;\\nconst files = ctx.request.body.files.images as File[];\\n\\n// After\\nconst name = ctx.request.body.name;\\n// file is of type \\"File\\"\\nconst file = ctx.files.get(\'avatar\')[0];\\n// files is of type \\"Files\\"\\nconst files = ctx.files.get(\'images\');\\n```\\n\\n### Authentication\\n\\nIn version 2, the `user` option of `@UseSessions` and `@JWTRequired` expected a function with this signature:\\n\\n```typescript\\n(id: string|number, services: ServiceManager) => Promise;\\n```\\n\\nThere was no way to guess and guarantee the type of the user ID and the function had to check and convert the type itself if necessary.\\n\\nThe returned type was also very permissive (type `any`) preventing us from detecting silly errors such as confusion between `null` and `undefined` values.\\n\\nIn version 3, the hooks have been added a new `userIdType` option to check and convert the JavaScript type if necessary and force the TypeScript type of the function. The returned type is also safer and corresponds to the type of `ctx.user` which is no longer `any` but `{ [key : string] : any } | null`.\\n\\n*Example where the ID is a string*\\n```typescript\\n@JWTRequired({\\n user: (id: string) => User.findOneBy({ id });\\n userIdType: \'string\',\\n})\\n```\\n\\n*Example where the ID is a number*\\n```typescript\\n@JWTRequired({\\n user: (id: number) => User.findOneBy({ id });\\n userIdType: \'number\',\\n})\\n```\\n\\nBy default, the value of `userIdType` is a number, so we can simply write this: \\n\\n```typescript\\n@JWTRequired({\\n user: (id: number) => User.findOneBy({ id });\\n})\\n```\\n\\n### GraphQL\\n\\nIn version 2, GraphQL schemas were of type `any`. In version 3, they are all based on the `GraphQLSchema` interface.\\n\\n## Closer to JS ecosystem standards\\n\\nSome parts have been modified to get closer to the JS ecosystem standards. In particular:\\n\\n### Development command\\n\\nThe `npm run develop` has been renamed to `npm run dev`.\\n\\n### Configuration through environment variables\\n\\nWhen two values of the same variable are provided by a `.env` file and an environment variable, then the value of the environment is used (the behavior is similar to that of the [dotenv](https://www.npmjs.com/package/dotenv) library).\\n\\n### `null` vs `undefined` values\\n\\nWhen the request has no session or the user is not authenticated, the values of `ctx.session` and `ctx.user` are `null` and no longer `undefined`. This makes sense from a semantic point of view, and it also simplifies the user assignment from the `find` functions of popular ORMs (Prisma, TypeORM, Mikro-ORM). They all return `null` when no value is found.\\n\\n## More open to other ORMs\\n\\nTypeORM is the default ORM used in the documentation examples and in the projects generated by the CLI. But it is quite possible to use another ORM or query generator with Foal. For example, the authentication system (with sessions or JWT) makes no assumptions about database access.\\n\\nSome parts of the framework were still a bit tied to TypeORM in version 2. Version 3 fixed this.\\n\\n### Shell scripts\\n\\nWhen running the `foal generate script` command, the generated script file no longer contains TypeORM code.\\n\\n### Permission system\\n\\nThe `@PermissionRequired` option is no longer bound to TypeORM and can be used with any `ctx.user` that implements the `IUserWithPermissions` interface.\\n\\n## Smaller AWS S3 package\\n\\nThe `@foal/aws-s3` package is now based on version 3 of the AWS SDK. Thanks to this, the size of the `node_modules` has been reduced by three.\\n\\n## Dependencies updated and support of Node latest versions\\n\\nAll Foal\'s dependencies have been upgraded. The framework is also tested on Node versions 16 and 18.\\n\\n## Some bug fixes\\n\\nIf the configuration file `production.js` explicitly returns `undefined` for a given key and the `default.json` file returns a defined value for this key, then the value from the `default.json` file is returned by `Config.get`."},{"id":"/2022/10/09/version-2.11-release-notes","metadata":{"permalink":"/blog/2022/10/09/version-2.11-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-10-09-version-2.11-release-notes.md","source":"@site/blog/2022-10-09-version-2.11-release-notes.md","title":"Version 2.11 release notes","description":"Banner","date":"2022-10-09T00:00:00.000Z","formattedDate":"October 9, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.975,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.11 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.11-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.0 release notes","permalink":"/blog/2022/11/01/version-3.0-release-notes"},"nextItem":{"title":"Version 2.10 release notes","permalink":"/blog/2022/08/11/version-2.10-release-notes"}},"content":"![Banner](./assets/version-2.11-is-here/banner.png)\\n\\nVersion 2.11 of [Foal](https://foalts.org/) is out! Here are the improvements that it brings:\\n\\n\x3c!--truncate--\x3e\\n\\n## Number of Iterations on Password Hashing Has Been Increased\\n\\nThe PBKDF2 algorithm (used for password hashing) uses a number of iterations to hash passwords. This work factor is deliberate and slows down potential attackers, making attacks against hashed passwords more difficult.\\n\\nAs computing power increases, the number of iterations must also increase. This is why, starting with version 2.11, the number of iterations has been increased to 310,000.\\n\\nTo check that an existing password hash is using the latest recommended number of iterations, you can use the `passwordHashNeedsToBeRefreshed` function.\\n\\nThe example below shows how to perform this check during a login and how to upgrade the password hash if the number of iterations turns out to be too low.\\n\\n```typescript\\nconst { email, password } = ctx.request.body;\\n\\nconst user = await User.findOne({ email });\\n\\nif (!user) {\\n return new HttpResponseUnauthorized();\\n}\\n\\nif (!await verifyPassword(password, user.password)) {\\n return new HttpResponseUnauthorized();\\n}\\n\\n// highlight-start\\n// This line must be after the password verification.\\nif (passwordHashNeedsToBeRefreshed(user.password)) {\\n user.password = await hashPassword(password);\\n await user.save();\\n}\\n// highlight-end\\n\\n// Log the user in.\\n```"},{"id":"/2022/08/11/version-2.10-release-notes","metadata":{"permalink":"/blog/2022/08/11/version-2.10-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-08-11-version-2.10-release-notes.md","source":"@site/blog/2022-08-11-version-2.10-release-notes.md","title":"Version 2.10 release notes","description":"Banner","date":"2022-08-11T00:00:00.000Z","formattedDate":"August 11, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.695,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.10 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.10-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.11 release notes","permalink":"/blog/2022/10/09/version-2.11-release-notes"},"nextItem":{"title":"FoalTS 2022 survey is open!","permalink":"/blog/2022/06/13/FoalTS-2022-survey-is-open"}},"content":"![Banner](./assets/version-2.10-is-here/banner.png)\\n\\nVersion 2.10 of [Foal](https://foalts.org/) is out! This small release brings some tiny improvements.\\n\\n\x3c!--truncate--\x3e\\n\\n## `@foal/cli` package included by default as dev dependency\\n\\nIssue: [#1097](https://github.com/FoalTS/foal/issues/1097)\\n\\nThe `@foal/cli` package is now installed by default as dev dependency. In this way, all commands of `package.json` still work when deploying the application to a Cloud provider that does not have the CLI installed globally.\\n\\nContributor: [@scho-to](https://github.com/scho-to/)\\n\\n## Preventing the `npm run develop` command to get stuck on some OS\\n\\nIssues: [#1022](https://github.com/FoalTS/foal/issues/1022), [#1115](https://github.com/FoalTS/foal/issues/1115)\\n\\nThe `npm run develop` was getting stuck on some OS based on the configuration of the app. This issue is now fixed in new projects. For current applications, you will need to add a `-r` flag to the `package.json` commands using `concurrently`.\\n\\n## Smaller `main` function\\n\\nThe `main` function that bootstraps the application is now smaller in new projects."},{"id":"/2022/06/13/FoalTS-2022-survey-is-open","metadata":{"permalink":"/blog/2022/06/13/FoalTS-2022-survey-is-open","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-06-13-FoalTS-2022-survey-is-open.md","source":"@site/blog/2022-06-13-FoalTS-2022-survey-is-open.md","title":"FoalTS 2022 survey is open!","description":"FoalTS 2022 survey is now open (yes, a few months late \ud83d\ude43)!","date":"2022-06-13T00:00:00.000Z","formattedDate":"June 13, 2022","tags":[{"label":"survey","permalink":"/blog/tags/survey"}],"readingTime":0.325,"hasTruncateMarker":false,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"FoalTS 2022 survey is open!","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","tags":["survey"]},"unlisted":false,"prevItem":{"title":"Version 2.10 release notes","permalink":"/blog/2022/08/11/version-2.10-release-notes"},"nextItem":{"title":"Version 2.9 release notes","permalink":"/blog/2022/05/29/version-2.9-release-notes"}},"content":"FoalTS 2022 survey is now open (yes, a few months late \ud83d\ude43)!\\n\\nYour responses to these questions are really valuable as they help me better understand what you need and how to improve the framework going forward \ud83d\udc4c.\\n\\nI read every response carefully so feel free to say anything you have to say!\\n\\n\ud83d\udc49 [The link to the survey](https://forms.gle/3HAzQboxSBXvpJbB6).\\n\\nThe survey closes on June 31."},{"id":"/2022/05/29/version-2.9-release-notes","metadata":{"permalink":"/blog/2022/05/29/version-2.9-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-05-29-version-2.9-release-notes.md","source":"@site/blog/2022-05-29-version-2.9-release-notes.md","title":"Version 2.9 release notes","description":"Banner","date":"2022-05-29T00:00:00.000Z","formattedDate":"May 29, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.19,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.9 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.9-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"FoalTS 2022 survey is open!","permalink":"/blog/2022/06/13/FoalTS-2022-survey-is-open"},"nextItem":{"title":"Version 2.8 release notes","permalink":"/blog/2022/02/13/version-2.8-release-notes"}},"content":"![Banner](./assets/version-2.9-is-here/banner.png)\\n\\nVersion 2.9 of [Foal](https://foalts.org/) has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## New OAuth2 Twitter Provider\\n\\nAfter LinkedIn, Google, Github and Facebook, Foal now supports Twitter for social authentication.\\n\\n\ud83d\udc49 [Link to the documentation](https://foalts.org/docs/authentication/social-auth/)\\n\\nA big thanks to [@LeonardoSalvucci](https://github.com/LeonardoSalvucci) for having implemented this feature.\\n\\n```typescript\\n// 3p\\nimport { Context, dependency, Get } from \'@foal/core\';\\nimport { TwitterProvider } from \'@foal/social\';\\n\\nexport class AuthController {\\n @dependency\\n twitter: TwitterProvider;\\n\\n @Get(\'/signin/twitter\')\\n redirectToTwitter() {\\n // Your \\"Login In with Twitter\\" button should point to this route.\\n // The user will be redirected to Twitter auth page.\\n return this.twitter.redirect();\\n }\\n\\n @Get(\'/signin/twitter/callback\')\\n async handleTwitterRedirection(ctx: Context) {\\n // Once the user gives their permission to log in with Twitter, the OAuth server\\n // will redirect the user to this route. This route must match the redirect URI.\\n const { userInfo, tokens } = await this.twitter.getUserInfo(ctx);\\n\\n // Do something with the user information AND/OR the access token.\\n // If you only need the access token, you can call the \\"getTokens\\" method.\\n\\n // The method usually ends with a HttpResponseRedirect object as returned value.\\n }\\n\\n}\\n```\\n\\n## OAuth2 Providers support PKCE Code Flow\\n\\nOAuth2 abstract provider now supports PKCE code flow. If you wish to implement your own provider using PKCE, it\'s now possible!\\n\\n## Support for version 15 of `graphql` and latest version of `type-graphql`\\n\\nFoal\'s dependencies have been updated so as to support the latest version of [TypeGraphQL](https://typegraphql.com/)."},{"id":"/2022/02/13/version-2.8-release-notes","metadata":{"permalink":"/blog/2022/02/13/version-2.8-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-02-13-version-2.8-release-notes.md","source":"@site/blog/2022-02-13-version-2.8-release-notes.md","title":"Version 2.8 release notes","description":"Banner","date":"2022-02-13T00:00:00.000Z","formattedDate":"February 13, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":9.735,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.8 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.8-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.9 release notes","permalink":"/blog/2022/05/29/version-2.9-release-notes"},"nextItem":{"title":"Version 2.7 release notes","permalink":"/blog/2021/12/12/version-2.7-release-notes"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/version-2.8-is-here/banner.png)\\n\\nVersion 2.8 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## WebSocket support and `socket.io` integration\\n\\nAs of version 2.8, Foal officially supports WebSockets, allowing you to establish two-way interactive communication between your server(s) and your clients.\\n\\nThe architecture includes: controllers and sub-controllers, hooks, success and error responses, message broadcasting, rooms, use from HTTP controllers, DI, error-handling, validation, unit testing, horizontal scalability, auto-reconnection, etc\\n\\n### Get Started\\n\\n#### Server\\n\\n```bash\\nnpm install @foal/socket.io\\n```\\n\\n*services/websocket.service.ts*\\n```typescript\\nimport { EventName, ValidatePayload, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n @EventName(\'create product\')\\n @ValidatePayload({\\n additionalProperties: false,\\n properties: { name: { type: \'string\' }},\\n required: [ \'name\' ],\\n type: \'object\'\\n })\\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\\n const product = new Product();\\n product.name = payload.name;\\n await product.save();\\n\\n // Send a message to all clients.\\n ctx.socket.broadcast.emit(\'refresh products\');\\n return new WebsocketResponse();\\n }\\n\\n}\\n```\\n\\n*src/index.ts*\\n\\n```typescript\\n// ...\\n\\nasync function main() {\\n const serviceManager = new ServiceManager();\\n\\n const app = await createApp(AppController, { serviceManager });\\n const httpServer = http.createServer(app);\\n\\n // Instanciate, init and connect websocket controllers.\\n await serviceManager.get(WebsocketController).attachHttpServer(httpServer);\\n\\n // ...\\n}\\n\\n```\\n\\n#### Client\\n\\n> This example uses JavaScript code as client, but socket.io supports also [many other languages](https://socket.io/docs/v4) (python, java, etc).\\n\\n```bash\\nnpm install socket.io-client@4\\n```\\n\\n```typescript\\nimport { io } from \'socket.io-client\';\\n\\nconst socket = io(\'ws://localhost:3001\');\\n\\nsocket.on(\'connect\', () => {\\n\\n socket.emit(\'create product\', { name: \'product 1\' }, response => {\\n if (response.status === \'error\') {\\n console.log(response.error);\\n }\\n });\\n\\n});\\n\\nsocket.on(\'connect_error\', () => {\\n console.log(\'Impossible to establish the socket.io connection\');\\n});\\n\\nsocket.on(\'refresh products\', () => {\\n console.log(\'refresh products!\');\\n});\\n```\\n\\n> When using socket.io with FoalTS, the client function `emit` can only take one, two or three arguments.\\n> ```typescript\\n> socket.emit(\'event name\');\\n> socket.emit(\'event name\', { /* payload */ });\\n> // The acknowledgement callback must always be passed in third position.\\n> socket.emit(\'event name\', { /* payload */ }, response => { /* do something */ });\\n> ```\\n\\n### Architecture\\n\\n#### Controllers and hooks\\n\\nThe WebSocket architecture is very similar to the HTTP architecture. They both have controllers and hooks. While HTTP controllers use paths to handle the various application endpoints, websocket controllers use event names. As with HTTP, event names can be extended with subcontrollers.\\n\\n*user.controller.ts*\\n```typescript\\nimport { EventName, WebsocketContext } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n createUser(ctx: WebsocketContext) {\\n // ...\\n }\\n\\n @EventName(\'delete\')\\n deleteUser(ctx: WebsocketContext) {\\n // ...\\n }\\n\\n}\\n```\\n\\n*websocket.controller.ts*\\n```typescript\\nimport { SocketIOController, wsController } from \'@foal/socket.io\';\\n\\nimport { UserController } from \'./user.controller.ts\';\\n\\nexport class WebsocketController extends SocketIOController {\\n subControllers = [\\n wsController(\'users \', UserController)\\n ];\\n}\\n```\\n\\n> Note that the event names are simply concatenated. So you have to manage the spaces between the words yourself if there are any.\\n\\n##### Contexts\\n\\nThe `Context` and `WebsocketContext` classes share common properties such as the `state`, the `user` and the `session`.\\n\\n\\nHowever, unlike their HTTP version, instances of `WebsocketContext` do not have a `request` property but a `socket` property which is the object provided by socket.io. They also have two other attributes: the `eventName` and the `payload` of the request.\\n\\n##### Responses\\n\\nA controller method returns a response which is either a `WebsocketResponse` or a `WebsocketErrorResponse`.\\n\\nIf a `WebsocketResponse(data)` is returned, the server will return to the client an object of this form:\\n```typescript\\n{\\n status: \'ok\',\\n data: data\\n}\\n```\\n\\n\\nIf it is a `WebsocketErrorResponse(error)`, the returned object will look like this:\\n```typescript\\n{\\n status: \'error\',\\n error: error\\n}\\n```\\n\\n> Note that the `data` and `error` parameters are both optional.\\n\\n##### Hooks\\n\\nIn the same way, Foal provides hooks for websockets. They work the same as their HTTP version except that some types are different (`WebsocketContext`, `WebsocketResponse|WebsocketErrorResponse`).\\n\\n```typescript\\nimport { EventName, WebsocketContext, WebsocketErrorResponse, WebsocketHook } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n @WebsocketHook((ctx, services) => {\\n if (typeof ctx.payload.name !== \'string\') {\\n return new WebsocketErrorResponse(\'Invalid name type\');\\n }\\n })\\n createUser(ctx: WebsocketContext) {\\n // ...\\n }\\n}\\n```\\n\\n##### Summary table\\n\\n| HTTP | Websocket |\\n| --- | --- |\\n| `@Get`, `@Post`, etc | `@EventName` |\\n| `controller` | `wsController` |\\n| `Context` | `WebsocketContext` |\\n| `HttpResponse`(s) | `WebsocketResponse`, `WebsocketErrorResponse` |\\n| `Hook` | `WebsocketHook` |\\n| `MergeHooks` | `MergeWebsocketHooks` |\\n| `getHookFunction`, `getHookFunctions` | `getWebsocketHookFunction`, `getWebsocketHookFunctions` |\\n\\n#### Send a message\\n\\nAt any time, the server can send one or more messages to the client using its `socket` object.\\n\\n*Server code*\\n```typescript\\nimport { EventName, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n createUser(ctx: WebsocketContext) {\\n ctx.socket.emit(\'event 1\', \'first message\');\\n ctx.socket.emit(\'event 1\', \'second message\');\\n return new WebsocketResponse();\\n }\\n}\\n```\\n\\n*Client code*\\n```typescript\\nsocket.on(\'event 1\', payload => {\\n console.log(\'Message: \', payload);\\n});\\n```\\n\\n#### Broadcast a message\\n\\nIf a message is to be broadcast to all clients, you can use the `broadcast` property for this.\\n\\n*Server code*\\n```typescript\\nimport { EventName, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n createUser(ctx: WebsocketContext) {\\n ctx.socket.broadcast.emit(\'event 1\', \'first message\');\\n ctx.socket.broadcast.emit(\'event 1\', \'second message\');\\n return new WebsocketResponse();\\n }\\n}\\n```\\n\\n*Client code*\\n```typescript\\nsocket.on(\'event 1\', payload => {\\n console.log(\'Message: \', payload);\\n});\\n```\\n\\n#### Grouping clients in rooms\\n\\nSocket.io uses the concept of [rooms](https://socket.io/docs/v4/rooms/) to gather clients in groups. This can be useful if you need to send a message to a particular subset of clients.\\n\\n```typescript\\nimport { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n onConnection(ctx: WebsocketContext) {\\n ctx.socket.join(\'some room\');\\n }\\n\\n @EventName(\'event 1\')\\n createUser(ctx: WebsocketContext) {\\n ctx.socket.to(\'some room\').emit(\'event 2\');\\n return new WebsocketResponse();\\n }\\n\\n}\\n```\\n\\n#### Accessing the socket.io server\\n\\nYou can access the socket.io server anywhere in your code (including your HTTP controllers) by injecting the `WsServer` service.\\n\\n```typescript\\nimport { dependency, HttpResponseOK, Post } from \'@foal/core\';\\nimport { WsServer } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n @dependency\\n wsServer: WsServer;\\n\\n @Post(\'/users\')\\n createUser() {\\n // ...\\n this.wsServer.io.emit(\'refresh users\');\\n\\n return new HttpResponseOK();\\n }\\n}\\n```\\n\\n#### Error-handling\\n\\nAny error thrown or rejected in a websocket controller, hook or service, if not caught, is converted to a `WebsocketResponseError`. If the `settings.debug` configuration parameter is `true`, then the error is returned as is to the client. Otherwise, the server returns this response:\\n\\n```typescript\\n({\\n status: \'error\',\\n error: {\\n code: \'INTERNAL_SERVER_ERROR\',\\n message: \'An internal server error has occurred.\'\\n }\\n})\\n```\\n\\n##### Customizing the error handler\\n\\nJust as its HTTP version, the `SocketIOController` class supports an optional `handleError` to override the default error handler.\\n\\n```typescript\\nimport { EventName, renderWebsocketError, SocketIOController, WebsocketContext, WebsocketErrorResponse } from \'@foal/socket.io\';\\n\\nclass PermissionDenied extends Error {}\\n\\nexport class WebsocketController extends SocketIOController implements ISocketIOController {\\n @EventName(\'create user\')\\n createUser() {\\n throw new PermissionDenied();\\n }\\n\\n handleError(error: Error, ctx: WebsocketContext){\\n if (error instanceof PermissionDenied) {\\n return new WebsocketErrorResponse(\'Permission is denied\');\\n }\\n\\n return renderWebsocketError(error, ctx);\\n }\\n}\\n```\\n\\n### Payload Validation\\n\\nFoal provides a default hook `@ValidatePayload` to validate the request payload. It is very similar to its HTTP version `@ValidateBody`.\\n\\n*Server code*\\n```typescript\\nimport { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n @EventName(\'create product\')\\n @ValidatePayload({\\n additionalProperties: false,\\n properties: { name: { type: \'string\' }},\\n required: [ \'name\' ],\\n type: \'object\'\\n })\\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\\n const product = new Product();\\n product.name = payload.name;\\n await product.save();\\n\\n // Send a message to all clients.\\n ctx.socket.broadcast.emit(\'refresh products\');\\n return new WebsocketResponse();\\n }\\n\\n}\\n```\\n\\n*Validation error response*\\n```typescript\\n({\\n status: \'error\',\\n error: {\\n code: \'VALIDATION_PAYLOAD_ERROR\',\\n payload: [\\n // errors\\n ]\\n }\\n})\\n```\\n\\n### Unit Testing\\n\\nTesting WebSocket controllers and hooks is very similar to testing their HTTP equivalent. The `WebsocketContext` takes three parameters.\\n\\n| Name | Type | Description |\\n| --- | --- | --- |\\n| `eventName` | `string` | The name of the event. |\\n| `payload`| `any` | The request payload. |\\n| `socket` | `any` | The socket (optional). Default: `{}`. |\\n\\n### Advanced\\n\\n#### Multiple node servers\\n\\nThis example shows how to manage multiple node servers using a redis adapter.\\n\\n```bash\\nnpm install @socket.io/redis-adapter@7 redis@3\\n```\\n\\n*websocket.controller.ts*\\n```typescript\\nimport { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\nimport { createAdapter } from \'@socket.io/redis-adapter\';\\nimport { createClient } from \'redis\';\\n\\nexport const pubClient = createClient({ url: \'redis://localhost:6379\' });\\nexport const subClient = pubClient.duplicate();\\n\\nexport class WebsocketController extends SocketIOController {\\n adapter = createAdapter(pubClient, subClient);\\n\\n @EventName(\'create user\')\\n createUser(ctx: WebsocketContext) {\\n // Broadcast an event to all clients of all servers.\\n ctx.socket.broadcast.emit(\'refresh users\');\\n return new WebsocketResponse();\\n }\\n}\\n```\\n\\n#### Handling connection\\n\\nIf you want to run some code when a Websocket connection is established (for example to join a room or forward the session), you can use the `onConnection` method of the `SocketIOController` for this.\\n\\n```typescript\\nimport { SocketIOController, WebsocketContext } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n onConnection(ctx: WebsocketContext) {\\n // ...\\n }\\n\\n}\\n```\\n\\n> The context passed in the `onConnection` method has an undefined payload and an empty event name.\\n\\n##### Error-handling\\n\\nAny errors thrown or rejected in the `onConnection` is sent back to the client. So you may need to add a `try {} catch {}` in some cases.\\n\\nThis error can be read on the client using the `connect_error` event listener.\\n\\n```typescript\\nsocket.on(\\"connect_error\\", () => {\\n // Do some stuff\\n socket.connect();\\n});\\n```\\n\\n#### Custom server options\\n\\nCustom options can be passed to the socket.io server as follows. The complete list of options can be found [here](https://socket.io/docs/v4/server-options/).\\n\\n```typescript\\nimport { SocketIOController } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n options = {\\n connectTimeout: 60000\\n }\\n\\n}\\n```\\n\\n## Passing a custom database client to a session store\\n\\nBy default, the `MongoDBStore` and `RedisStore` create a new client to connect to their respective databases. The `TypeORMStore` uses the default TypeORM connection.\\n\\nIt is now possible to override this behavior by providing a custom client to the stores at initialization.\\n\\n### `TypeORMStore`\\n\\n*First example*\\n```typescript\\nimport { dependency } from \'@foal/core\';\\nimport { TypeORMStore } from \'@foal/typeorm\';\\nimport { createConnection } from \'typeorm\';\\n\\nexport class AppController {\\n @dependency\\n store: TypeORMStore;\\n\\n // ...\\n\\n async init() {\\n const connection = await createConnection(\'connection2\');\\n this.store.setConnection(connection);\\n }\\n}\\n```\\n\\n*Second example*\\n\\n```typescript\\nimport { createApp, ServiceManager } from \'@foal/core\';\\nimport { TypeORMStore } from \'@foal/typeorm\';\\nimport { createConnection } from \'typeorm\';\\n\\nasync function main() {\\n const connection = await createConnection(\'connection2\');\\n\\n const serviceManager = new ServiceManager();\\n serviceManager.get(TypeORMStore).setConnection(connection);\\n\\n const app = await createApp(AppController, { serviceManager });\\n\\n // ...\\n}\\n```\\n\\n### `RedisStore`\\n\\n```\\nnpm install redis@3\\n```\\n\\n*index.ts*\\n```typescript\\nimport { createApp, ServiceManager } from \'@foal/core\';\\nimport { RedisStore } from \'@foal/redis\';\\nimport { createClient } from \'redis\';\\n\\nasync function main() {\\n const redisClient = createClient(\'redis://localhost:6379\');\\n\\n const serviceManager = new ServiceManager();\\n serviceManager.get(RedisStore).setRedisClient(redisClient);\\n\\n const app = await createApp(AppController, { serviceManager });\\n\\n // ...\\n}\\n```\\n\\n### `MongoDBStore`\\n\\n```\\nnpm install mongodb@3\\n```\\n\\n*index.ts*\\n```typescript\\nimport { createApp, ServiceManager } from \'@foal/core\';\\nimport { MongoDBStore } from \'@foal/mongodb\';\\nimport { MongoClient } from \'mongodb\';\\n\\nasync function main() {\\n const mongoDBClient = await MongoClient.connect(\'mongodb://localhost:27017/db\', {\\n useNewUrlParser: true,\\n useUnifiedTopology: true\\n });\\n\\n const serviceManager = new ServiceManager();\\n serviceManager.get(MongoDBStore).setMongoDBClient(mongoDBClient);\\n\\n const app = await createApp(AppController, { serviceManager });\\n\\n // ...\\n}\\n```\\n\\n## Support for AWS S3 Server-Side Encryption\\n\\nA new configuration option can be provided to the `S3Disk` to support server-side encryption.\\n\\n*Example*\\n\\n\\n\\n```yaml\\nsettings:\\n aws:\\n accessKeyId: xxx\\n secretAccessKey: yyy\\n disk:\\n driver: \'@foal/aws-s3\'\\n s3:\\n bucket: \'uploaded\'\\n serverSideEncryption: \'AES256\'\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"aws\\": {\\n \\"accessKeyId\\": \\"xxx\\",\\n \\"secretAccessKey\\": \\"yyy\\"\\n },\\n \\"disk\\": {\\n \\"driver\\": \\"@foal/aws-s3\\",\\n \\"s3\\": {\\n \\"bucket\\": \\"uploaded\\",\\n \\"serverSideEncryption\\": \\"AES256\\"\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n aws: {\\n accessKeyId: \\"xxx\\",\\n secretAccessKey: \\"yyy\\"\\n },\\n disk: {\\n driver: \\"@foal/aws-s3\\",\\n s3: {\\n bucket: \\"uploaded\\",\\n serverSideEncryption: \\"AES256\\"\\n }\\n }\\n }\\n}\\n```\\n\\n\\n"},{"id":"/2021/12/12/version-2.7-release-notes","metadata":{"permalink":"/blog/2021/12/12/version-2.7-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-12-12-version-2.7-release-notes.md","source":"@site/blog/2021-12-12-version-2.7-release-notes.md","title":"Version 2.7 release notes","description":"Banner","date":"2021-12-12T00:00:00.000Z","formattedDate":"December 12, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.83,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.7 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.7-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.8 release notes","permalink":"/blog/2022/02/13/version-2.8-release-notes"},"nextItem":{"title":"Version 2.6 release notes","permalink":"/blog/2021/09/19/version-2.6-release-notes"}},"content":"![Banner](./assets/version-2.7-is-here/banner.png)\\n\\nVersion 2.7 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## The body of `HttpResponse` can be typed\\n\\nThe `HttpResponse` class becomes generic so as to enforce the type of its `body` property if needed.\\n\\n```typescript\\nimport { Get, HttpResponse } from \'@foal/core\';\\n\\nimport { Product } from \'../entities\';\\n\\nexport class AppController {\\n @Get(\'/products\')\\n async products(): HttpResponse {\\n const products = await Product.find({});\\n return new HttpResponse(products);\\n }\\n}\\n```\\n\\nIt also allows you to infer the type of the body in your tests:\\n\\n![Generic HttpResponse](./assets/version-2.7-is-here/generic-http-response.png)\\n\\n## Support for signed cookies\\n\\nStarting from this version, you can sign cookies and read them through the `signedCookies` attribute.\\n\\n```typescript\\nimport { Context, HttpResponseOK, Get, Post } from \'@foal/core\';\\n\\nclass AppController {\\n @Get(\'/\')\\n index(ctx: Context) {\\n const cookie1: string|undefined = ctx.request.signedCookies.cookie1;\\n // Do something.\\n return new HttpResponseOK();\\n }\\n\\n @Post(\'/sign-cookie\')\\n index() {\\n return new HttpResponseOK()\\n .setCookie(\'cookie1\', \'value1\', {\\n signed: true\\n });\\n }\\n}\\n```\\n\\n> In order to use signed cookies, you must provide a secret with the configuration key `settings.cookieParser.secret`.\\n\\n## Environment name can be provided via `NODE_ENV` or `FOAL_ENV`\\n\\nVersion 2.7 allows to you to specify the environment name (production, development, etc) with the `FOAL_ENV` environment variable.\\n\\nThis can be useful if you have third party libraries whose behavior also depends on the value of `NODE_ENV` (see [Github issue here](https://github.com/FoalTS/foal/issues/1004)).\\n\\n## `foal generate entity` and `foal generate hook` support sub-directories\\n\\n### Example with entities (models)\\n\\n```shell\\nfoal g entity user\\nfoal g entity business/product\\n```\\n\\n*Output*\\n```\\nsrc/\\n \'- app/\\n \'- entities/\\n |- business/\\n | |- product.entity.ts\\n | \'- index.ts\\n |- user.entity.ts\\n \'- index.ts\\n```\\n\\n### Example with hooks\\n\\n```shell\\nfoal g hook log\\nfoal g hook auth/admin-required\\n```\\n\\n*Output*\\n```\\nsrc/\\n \'- app/\\n \'- hooks/\\n |- auth/\\n | |- admin-required.hook.ts\\n | \'- index.ts\\n |- log.hook.ts\\n \'- index.ts\\n```\\n\\n## New `afterPreMiddlewares` option in `createApp`\\n\\nIt is now possible to run a custom middleware after all internal Express middlewares of the framework.\\n\\nThis can be useful in rare situations, for example when using the [RequestContext helper](https://mikro-orm.io/docs/identity-map/#-requestcontext-helper-for-di-containers) in Mikro-ORM.\\n\\n```typescript\\nconst app = await createApp({\\n afterPreMiddlewares: [\\n (req, res, next) => {\\n RequestContext.create(orm.em, next);\\n }\\n ]\\n})\\n```\\n\\n\\n## Contributors\\n\\n- [@MCluck90](https://github.com/MCluck90)\\n- [@kingdun3284](https://github.com/kingdun3284)"},{"id":"/2021/09/19/version-2.6-release-notes","metadata":{"permalink":"/blog/2021/09/19/version-2.6-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-09-19-version-2.6-release-notes.md","source":"@site/blog/2021-09-19-version-2.6-release-notes.md","title":"Version 2.6 release notes","description":"Banner","date":"2021-09-19T00:00:00.000Z","formattedDate":"September 19, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.385,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.6 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.6-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.7 release notes","permalink":"/blog/2021/12/12/version-2.7-release-notes"},"nextItem":{"title":"Version 2.5 release notes","permalink":"/blog/2021/06/11/version-2.5-release-notes"}},"content":"![Banner](./assets/version-2.6-is-here/banner.png)\\n\\nVersion 2.6 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## Support of the `array` value for AJV `coerceTypes` option\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"ajv\\": {\\n \\"coerceTypes\\": \\"array\\"\\n }\\n }\\n}\\n```\\n\\nOption documentation: [https://ajv.js.org/coercion.html#coercion-to-and-from-array](https://ajv.js.org/coercion.html#coercion-to-and-from-array).\\n\\n## Swagger page supports strict CSP\\n\\nInline scripts in the Swagger page have been removed to support more strict *Content Security Policy* directive.\\n\\n## Bug fixes\\n\\nThe `foal connect angular` command now supports empty `angular.json` files."},{"id":"/2021/06/11/version-2.5-release-notes","metadata":{"permalink":"/blog/2021/06/11/version-2.5-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-06-11-version-2.5-release-notes.md","source":"@site/blog/2021-06-11-version-2.5-release-notes.md","title":"Version 2.5 release notes","description":"Banner","date":"2021-06-11T00:00:00.000Z","formattedDate":"June 11, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.525,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.5 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.5-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.6 release notes","permalink":"/blog/2021/09/19/version-2.6-release-notes"},"nextItem":{"title":"Version 2.4 release notes","permalink":"/blog/2021/05/19/version-2.4-release-notes"}},"content":"![Banner](./assets/version-2.5-is-here/banner.png)\\n\\nVersion 2.5 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## `npm run develop` watches config files\\n\\nIn previous versions of Foal, the `develop` command did not restart the server when a configuration file was changed. This was annoying and is the reason why, starting with v2.5, new projects generated by the CLI will watch configuration files.\\n\\n## `createOpenApiDocument` accepts an optional serviceManager\\n\\nIf you use `createOpenApiDocument`, in a shell script for example, the function accepts an optional `serviceManager` parameter from this version.\\n\\nThis can be useful if your OpenAPI decorators access controller properties whose values are manually injected."},{"id":"/2021/05/19/version-2.4-release-notes","metadata":{"permalink":"/blog/2021/05/19/version-2.4-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-05-19-version-2.4-release-notes.md","source":"@site/blog/2021-05-19-version-2.4-release-notes.md","title":"Version 2.4 release notes","description":"Banner","date":"2021-05-19T00:00:00.000Z","formattedDate":"May 19, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.01,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.4 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.4-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.5 release notes","permalink":"/blog/2021/06/11/version-2.5-release-notes"},"nextItem":{"title":"Version 2.3 release notes","permalink":"/blog/2021/04/22/version-2.3-release-notes"}},"content":"![Banner](./assets/version-2.4-is-here/banner.png)\\n\\nVersion 2.4 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## `$data` references for validation\\n\\nVersion 2.4 allows you to enable the AJV `$data` option so that you can use the verified data values as validators for other values.\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"ajv\\": {\\n \\"$data\\": true\\n }\\n }\\n}\\n```\\n\\n*Example of auth controller*\\n```typescript\\nimport { Context, Post, ValidateBody } from \'@foal/core\';\\n\\nexport class AuthController {\\n @Post(\'/signup\')\\n @ValidateBody({\\n type: \'object\',\\n properties: {\\n username: { type: \'string\' },\\n password: { type: \'string\' },\\n // \\"password\\" and \\"confirmPassword\\" should be identical.\\n confirmPassword: {\\n const: {\\n $data: \'1/password\',\\n },\\n type: \'string\',\\n },\\n }\\n required: [ \'username\', \'password\', \'confirmPassword\' ],\\n additionalProperties: false\\n })\\n signup(ctx: Context) {\\n // Do something.\\n }\\n}\\n\\n```\\n\\n## Cache option for file downloading\\n\\nStarting from version 2.4 the `Disk.createHttpResponse` method accepts an optional parameter to specify the value of the `Cache-Control` header.\\n\\n```typescript\\nimport { Context, dependency, Get } from \'@foal/core\';\\nimport { Disk } from \'@foal/storage\';\\n\\nimport { User } from \'../entities\';\\n\\nexport class ProfileController {\\n @dependency\\n disk: Disk;\\n\\n @Get(\'/avatar\')\\n async readProfileImage(ctx: Context) {\\n return this.disk.createHttpResponse(ctx.user.avatar, {\\n cache: \'no-cache\'\\n });\\n }\\n```\\n\\n## Bug fixes\\n\\nSee issue [#930](https://github.com/FoalTS/foal/issues/930).\\n\\n## Contributors\\n\\n[@ZakRabe](https://github.com/ZakRabe)"},{"id":"/2021/04/22/version-2.3-release-notes","metadata":{"permalink":"/blog/2021/04/22/version-2.3-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-04-22-version-2.3-release-notes.md","source":"@site/blog/2021-04-22-version-2.3-release-notes.md","title":"Version 2.3 release notes","description":"Banner","date":"2021-04-22T00:00:00.000Z","formattedDate":"April 22, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":2.07,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.3 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.3-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.4 release notes","permalink":"/blog/2021/05/19/version-2.4-release-notes"},"nextItem":{"title":"What\'s new in version 2 (part 4/4)","permalink":"/blog/2021/04/08/whats-new-in-version-2-part-4"}},"content":"![Banner](./assets/version-2.3-is-here/banner.png)\\n\\nVersion 2.3 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## GraphiQL\\n\\nFrom version 2.3, it is possible to generate a GraphiQL page in one line of code. This can be useful if you quickly need to test your API.\\n\\n```bash\\nnpm install @foal/graphiql\\n```\\n\\n![GraphiQL](./assets/version-2.3-is-here/graphiql.png)\\n\\n*app.controller.ts*\\n```typescript\\nimport { GraphiQLController } from \'@foal/graphiql\';\\n\\nimport { GraphqlApiController } from \'./services\';\\n\\nexport class AppController {\\n\\n subControllers = [\\n // ...\\n controller(\'/graphql\', GraphqlApiController),\\n controller(\'/graphiql\', GraphiQLController)\\n ];\\n\\n}\\n```\\n\\nThe page is also customizable and you can provide additional options to change the UI or the API endpoint.\\n\\n```typescript\\nexport class GraphiQL2Controller extends GraphiQLController {\\n\\n cssThemeURL = \'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/theme/solarized.css\';\\n\\n apiEndpoint = \'/api\';\\n\\n options: GraphiQLControllerOptions = {\\n docExplorerOpen: true,\\n editorTheme: \'solarized light\'\\n }\\n\\n}\\n\\n```\\n\\n## Support for `.env.local` files\\n\\nFoal\'s configuration system already supported `.env` files in previous versions. As of version 2.3, the framework also supports `.env.local` files.\\n\\nThis can be useful in case you want to have two `.env` files, one to define the default env vars needed by the application and another to override these values on your local machine.\\n\\nIf a variable is defined in both files, the value in the `.env.local` file will take precedence.\\n\\nSimilarly, you can also define environment-specific local files (`.env.development.local`, `.env.production.local`, etc).\\n\\n## Prisma documentation\\n\\nThe documentation has been expanded to include [examples](https://foalts.org/docs/databases/other-orm/introduction) of how to use Prisma with Foal.\\n\\n## Base 64 and base 64 URL utilities\\n\\nTwo functions are provided to convert base64 encoded strings to base64url encoded strings and vice versa.\\n\\n```typescript\\nimport { convertBase64ToBase64url, convertBase64urlToBase64 } from \'@foal/core\';\\n\\nconst str = convertBase64ToBase64url(base64Str);\\nconst str2 = convertBase64urlToBase64(base64urlStr);\\n```\\n\\n## Converting Streams to Buffers\\n\\nIn case you need to convert a readable stream to a concatenated buffer during testing, you can now use the `streamToBuffer` function for this.\\n\\n```typescript\\nimport { streamToBuffer } from \'@foal/core\';\\n\\nconst buffer = await streamToBuffer(stream);\\n```\\n\\n## Accessing services during authentication\\n\\nThe `user` option of `@JWTRequired` and `@UseSessions` now gives you the possibility to access services.\\n\\n```typescript\\nclass UserService {\\n getUser(id) {\\n return User.findOne({ id });\\n }\\n}\\n\\n@JWTRequired({\\n user: (id, services) => services.get(UserService).getUser(id)\\n})\\nclass ApiController {\\n @Get(\'/products\')\\n getProducts(ctx: Context) {\\n // ctx.user is the object returned by UserService.\\n }\\n}\\n\\n```\\n\\n## Bug Fixes\\n\\n### Social authentication\\n\\nSocial authentication controllers could sometimes return 500 errors, depending on the social provider you were using. This was due to a problem of string encoding in the callback URL. This bug has been fixed in this version."},{"id":"/2021/04/08/whats-new-in-version-2-part-4","metadata":{"permalink":"/blog/2021/04/08/whats-new-in-version-2-part-4","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-04-08-whats-new-in-version-2-part-4.md","source":"@site/blog/2021-04-08-whats-new-in-version-2-part-4.md","title":"What\'s new in version 2 (part 4/4)","description":"Banner","date":"2021-04-08T00:00:00.000Z","formattedDate":"April 8, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":5.675,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 4/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-4.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.3 release notes","permalink":"/blog/2021/04/22/version-2.3-release-notes"},"nextItem":{"title":"What\'s new in version 2 (part 3/4)","permalink":"/blog/2021/03/11/whats-new-in-version-2-part-3"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-4/banner.png)\\n\\nThis article presents the improvements to the session system in FoalTS version 2.\\n\\nThe new syntax can be used either with cookies or with the `Authorization` header. It adds the following new features:\\n- query all sessions of a given user\\n- query all connected users\\n- force logout of a specific user\\n- flash sessions\\n- session ID regeneration\\n- anonymous and authenticated sessions\\n\\nFoalTS also simplifies stateful CSRF protection so that all it takes is one setting to enable it.\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 4 of the series of articles *What\'s new in version 2.0*. Part 3 can be found [here](./2021-03-11-whats-new-in-version-2-part-3.md).\\n\\n## New Session System\\n\\nThe new authentication system is probably the main new feature of version 2. The old session components have been redesigned so as to serve three purposes:\\n- be easy to use with very little code,\\n- support a large variety of applications and architectures (SPA, Mobile, SSR, API, `Authorization` header, cookies, serverless environment, social auth, etc),\\n- and add missing features impossible to implement in version 1.\\n\\nHere is the way to use it:\\n- First [specify in the configuration](/docs/authentication/session-tokens#choosing-a-session-store) where your sessions should be stored (SQL database, redis, Mongo, etc).\\n- Then decorate any route or controller that need authentication with `@UseSessions`.\\n\\n### Example with the `Authorization` header\\n\\nIn this first example, we\'d like to use the `Authorization` header to handle authentication.\\n\\nWe want to send an email address and password to `/login` and retrieve a token in return to authenticate further requests.\\n\\n```typescript\\nimport { dependency, Context, Get, HttpResponseOK, UserRequired, UseSessions, ValidateBody, HttpResponseUnauthorized, Post } from \'@foal/core\';\\nimport { fetchUser } from \'@foal/typeorm\';\\n\\nimport { User, Product } from \'../entities\';\\n\\n@UseSessions({\\n user: fetchUser(User)\\n})\\nexport class ApiController {\\n @dependency\\n store: Store;\\n\\n @Get(\'/products\')\\n @UserRequired()\\n async readProducts(ctx: Context) {\\n return new HttpResponseOK(Product.find({ user: ctx.user }));\\n }\\n\\n @Post(\'/login\')\\n @ValidateBody({\\n additionalProperties: false,\\n properties: {\\n email: { type: \'string\', format: \'email\' },\\n password: { type: \'string\' }\\n },\\n required: [ \'email\', \'password\' ],\\n type: \'object\',\\n })\\n async login(ctx: Context) {\\n const user = await User.findOne({ email: ctx.request.body.email });\\n\\n if (!user) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n ctx.session = await createSession(this.store);\\n ctx.session.setUser(user);\\n\\n return new HttpResponseOK({\\n token: ctx.session.getToken()\\n });\\n }\\n\\n @Post(\'/logout\')\\n async logout(ctx: Context) {\\n if (ctx.session) {\\n await ctx.session.destroy();\\n }\\n\\n return new HttpResponseOK();\\n }\\n}\\n```\\n\\n### Example with cookies\\n\\nIn this second example, we will use cookies to manage authentication. Foal will auto-creates a session when none exists.\\n\\n```typescript\\nimport { dependency, Context, Get, HttpResponseOK, UserRequired, UseSessions, ValidateBody, HttpResponseUnauthorized, Post } from \'@foal/core\';\\nimport { fetchUser } from \'@foal/typeorm\';\\n\\nimport { User, Product } from \'../entities\';\\n\\n@UseSessions({\\n // highlight-next-line\\n cookie: true,\\n user: fetchUser(User)\\n})\\nexport class ApiController {\\n @dependency\\n store: Store;\\n\\n @Get(\'/products\')\\n @UserRequired()\\n async readProducts(ctx: Context) {\\n return new HttpResponseOK(Product.find({ user: ctx.user }));\\n }\\n\\n @Post(\'/login\')\\n @ValidateBody({\\n additionalProperties: false,\\n properties: {\\n email: { type: \'string\', format: \'email\' },\\n password: { type: \'string\' }\\n },\\n required: [ \'email\', \'password\' ],\\n type: \'object\',\\n })\\n async login(ctx: Context) {\\n const user = await User.findOne({ email: ctx.request.body.email });\\n\\n if (!user) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n // highlight-next-line\\n ctx.session.setUser(user);\\n\\n // highlight-next-line\\n return new HttpResponseOK();\\n }\\n\\n @Post(\'/logout\')\\n async logout(ctx: Context) {\\n if (ctx.session) {\\n await ctx.session.destroy();\\n }\\n\\n return new HttpResponseOK();\\n }\\n}\\n```\\n\\n### New features\\n\\nIn addition to this redesign, version 2 also offers new features.\\n\\n#### Query all sessions of a user (TypeORM only)\\n\\nThis feature allows you to list all sessions associated with a specific user. This can be useful if a user is connected on several devices and you like to audit them.\\n\\n```typescript\\nconst user = { id: 1 };\\nconst ids = await store.getSessionIDsOf(user);\\n```\\n\\n#### Query all connected users (TypeORM only)\\n\\nThis feature lists all users that have at least one session in the database.\\n\\n```typescript\\nconst ids = await store.getAuthenticatedUserIds();\\n```\\n\\n#### Force the disconnection of a user (TypeORM only)\\n\\nIn case you want to remove all sessions associated with a specific user, you can use the `destroyAllSessionsOf` method. This can be useful if you think a session has been corrupted or when you want, for example when a password is changed, to disconnect a user from all other devices to which he/she has previously logged on.\\n\\n```typescript\\nconst user = { id: 1 };\\nawait store.destroyAllSessionsOf(user);\\n```\\n\\n#### Flash sessions\\n\\nFlash content is used when we want to save data (a message for example) only for the next request. A typical use case is when a user enters wrong credentials. The page is refreshed and an error message is displayed.\\n\\nTo use flash content, you only need to add the option `flash` set to `true` in the `set` method.\\n\\n```typescript\\nctx.session.set(\'error\', \'Incorrect email or password\', { flash: true });\\n```\\n\\n#### Regenerate the session ID\\n\\nRegenerating the session ID is a [recommended practice](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change) when changing a user\'s privileges or password. This can now be done with the `regenerateID` method\\n\\n```typescript\\nawait ctx.session.regenerateID();\\n```\\n\\n#### Expired sessions clean up regularly (TypeORM and MongoDB)\\n\\nStarting from version 2, Foal regularly cleanup expired sessions in your database so you don\'t have to do it manually.\\n\\n#### Anonymous sessions and templates\\n\\nIn version 2, `@UseSessions({ cookie: true })` automatically creates a session if none exists. This is particularly useful if you\'re building a shopping website with SSR templates. When the user navigates on the website, he/she can add items to the cart without having to log in the first place. Then, when the user wants to place his/her order, he can log in and the only thing you have to do is this:\\n\\n```typescript\\nctx.session.setUser(user)\\n```\\n\\n## Stateful CSRF protection simplified\\n\\nIn version 1, providing a CSRF protection was quite complex. We needed to manage token generation, handle the CSRF cookie (expiration, etc), use an additional hook, etc.\\n\\nStarting from version 2, the CSRF protection is all managed by `@UseSessions`.\\n\\n\\n\\n\\n\\n```yaml\\nsettings:\\n session:\\n csrf:\\n enabled: true\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"session\\": {\\n \\"csrf\\": {\\n \\"enabled\\": true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n session: {\\n csrf: {\\n enabled: true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\nWhen it is enabled, an additional `XSRF-TOKEN` cookie is sent to the client at the same time as the session cookie. It contains a CSRF token associated with your session.\\n\\nWhen a request is made to the server, the `@UseSessions` hooks expects you to include its value in the `XSRF-TOKEN` header.\\n\\n> If you\'re building a regular web application and want to include the CSRF token in your templates, you can retrieve it with this statement: `ctx.session.get(\'csrfToken\')`."},{"id":"/2021/03/11/whats-new-in-version-2-part-3","metadata":{"permalink":"/blog/2021/03/11/whats-new-in-version-2-part-3","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-03-11-whats-new-in-version-2-part-3.md","source":"@site/blog/2021-03-11-whats-new-in-version-2-part-3.md","title":"What\'s new in version 2 (part 3/4)","description":"Banner","date":"2021-03-11T00:00:00.000Z","formattedDate":"March 11, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":3.665,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 3/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-3.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 4/4)","permalink":"/blog/2021/04/08/whats-new-in-version-2-part-4"},"nextItem":{"title":"What\'s new in version 2 (part 2/4)","permalink":"/blog/2021/03/02/whats-new-in-version-2-part-2"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-3/banner.png)\\n\\nThis article presents some improvements introduced in version 2 of FoalTS:\\n- the JWT utilities to manage secrets and RSA keys,\\n- the JWT utilities to manage cookies,\\n- and the new stateless CSRF protection.\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 3 of the series of articles *What\'s new in version 2.0*. Part 2 can be found [here](./2021-03-02-whats-new-in-version-2-part-2.md).\\n\\n## New JWT utilities\\n\\n### Accessing config secrets and public/private keys\\n\\nStarting from version 2, there is a standardized way to provide and retrieve JWT secrets and RSA public/private keys: the functions `getSecretOrPublicKey` and `getSecretOrPrivateKey`.\\n\\n#### Using secrets\\n\\nIn this example, a base64-encoded secret is provided in the configuration.\\n\\n*.env*\\n```\\nJWT_SECRET=\\"Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=\\"\\n```\\n\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n secret: \\"env(JWT_SECRET)\\"\\n secretEncoding: base64\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"secret\\": \\"env(JWT_SECRET)\\",\\n \\"secretEncoding\\": \\"base64\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n jwt: {\\n secret: \\"env(JWT_SECRET)\\",\\n secretEncoding: \\"base64\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\nBoth `getSecretOrPublicKey` and `getSecretOrPrivateKey` functions will return the secret.\\n\\nIn the case a `secretEncoding` value is provided, the functions return a buffer which is the secret decoded with the provided encoding.\\n\\n#### Using public and private keys\\n\\n```javascript\\nconst { Env } = require(\'@foal/core\');\\nconst { readFileSync } = require(\'fs\');\\n\\nmodule.exports = {\\n settings: {\\n jwt: {\\n privateKey: Env.get(\'RSA_PRIVATE_KEY\') || readFileSync(\'./id_rsa\', \'utf8\'),\\n publicKey: Env.get(\'RSA_PUBLIC_KEY\') || readFileSync(\'./id_rsa.pub\', \'utf8\'),\\n }\\n }\\n}\\n```\\n\\nIn this case, `getSecretOrPublicKey` and `getSecretOrPrivateKey` return the keys from the environment variables `RSA_PUBLIC_KEY` and `RSA_PRIVATE_KEY` if they are defined or from the files `id_rsa` and `id_rsa.pub` otherwise.\\n\\n### Managing cookies\\n\\nIn version 2, Foal provides two dedicated functions to manage JWT with cookies. Using these functions instead of manually setting the cookie has three benefits:\\n- they include a CSRF protection (see section below),\\n- the function `setAuthCookie` automatically sets the cookie expiration based on the token expiration,\\n- and cookie options can be provided through the configuration.\\n\\n**Example**\\n\\n*api.controller.ts*\\n```typescript\\nimport { JWTRequired } from \'@foal/jwt\';\\n\\n@JWTRequired({ cookie: true })\\nexport class ApiController {\\n // ...\\n}\\n```\\n\\n*auth.controller.ts*\\n```typescript\\nexport class AuthController {\\n\\n @Post(\'/login\')\\n async login(ctx: Context) {\\n // ...\\n\\n const response = new HttpResponseNoContent();\\n // Do not forget the \\"await\\" keyword.\\n await setAuthCookie(response, token);\\n return response;\\n }\\n\\n @Post(\'/logout\')\\n logout(ctx: Context) {\\n // ...\\n\\n const response = new HttpResponseNoContent();\\n removeAuthCookie(response);\\n return response;\\n }\\n\\n}\\n```\\n\\n**Cookie options**\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n cookie:\\n name: mycookiename # Default: auth\\n domain: example.com\\n httpOnly: true # Warning: unlike session tokens, the httpOnly directive has no default value.\\n path: /foo # Default: /\\n sameSite: strict # Default: lax if settings.jwt.csrf.enabled is true.\\n secure: true\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"cookie\\": {\\n \\"name\\": \\"mycookiename\\",\\n \\"domain\\": \\"example.com\\",\\n \\"httpOnly\\": true,\\n \\"path\\": \\"/foo\\",\\n \\"sameSite\\": \\"strict\\",\\n \\"secure\\": true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n jwt: {\\n cookie: {\\n name: \\"mycookiename\\",\\n domain: \\"example.com\\",\\n httpOnly: true,\\n path: \\"/foo\\",\\n sameSite: \\"strict\\",\\n secure: true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n## Stateless CSRF protection simplified\\n\\nIn version 1, providing a CSRF protection was quite complex. We needed to provide another secret, generate a stateless token, manage the CSRF cookie (expiration, etc), use an additional hook, etc.\\n\\nStarting from version 2, the CSRF protection is all managed by `@JWTRequired`, `setAuthCookie` and `removeAuthCookie`.\\n\\nThe only thing that you have to do it to enable it through the configuration:\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n csrf:\\n enabled: true\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"csrf\\": {\\n \\"enabled\\": true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n jwt: {\\n csrf: {\\n enabled: true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\nWhen it is enabled, an additional `XSRF-TOKEN` cookie is sent to the client at the same time as the auth cookie (containing your JWT). It contains a stateless CSRF token which is signed and has the same expiration date as your JWT.\\n\\nWhen a request is made to the server, the `@JWTRequired` hooks expects you to include its value in the `XSRF-TOKEN` header."},{"id":"/2021/03/02/whats-new-in-version-2-part-2","metadata":{"permalink":"/blog/2021/03/02/whats-new-in-version-2-part-2","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-03-02-whats-new-in-version-2-part-2.md","source":"@site/blog/2021-03-02-whats-new-in-version-2-part-2.md","title":"What\'s new in version 2 (part 2/4)","description":"Banner","date":"2021-03-02T00:00:00.000Z","formattedDate":"March 2, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":5.055,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 2/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-2.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 3/4)","permalink":"/blog/2021/03/11/whats-new-in-version-2-part-3"},"nextItem":{"title":"Version 2.2 release notes","permalink":"/blog/2021/02/25/version-2.2-release-notes"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-2/banner.png)\\n\\nThis article presents some improvements introduced in version 2 of FoalTS:\\n- Configuration and type safety\\n- Configuration and `.env` files (`.env`, `.env.test`, etc)\\n- Available configuration file formats (JSON, YAML and JS)\\n- OpenAPI schemas and validation\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 2 of the series of articles *What\'s new in version 2.0*. Part 1 can be found [here](./2021-02-17-whats-new-in-version-2-part-1.md).\\n\\n## New Config System\\n\\n### Type safety\\n\\nStarting from version 2, a great attention is paid to type safety in the configuration. The `Config.get` method allows you specify which type you expect.\\n\\n```typescript\\nconst timeout = Config.get(\'custom.timeout\', \'number\');\\n// The TypeScript type returned by `get` is number|undefined.\\n```\\n\\nIn this example, when calling the `get` method, the framework will look at the configuration files to retrieve the desired value.\\n- If the value is not defined, the function returns `undefined`.\\n- If the value is a number, the function returns it.\\n- If the value is a string that can be converted to a number (ex: `\\"1\\"`), the function converts and returns it.\\n- If the value is not a number and cannot be converted, then the function throws a `ConfigTypeError` with the details. Note that the config value is not logged to avoid leaking sensitive information.\\n\\nIf you wish to make the config parameter mandatory, you can do it by using the `getOrThrow` method. If no value is found, then a `ConfigNotFound` error is thrown.\\n\\n```typescript\\nconst timeout = Config.getOrThrow(\'custom.timeout\', \'number\');\\n// The TypeScript type returned by `get` is number.\\n```\\n\\nSupported types are `string`, `number`, `boolean`, `boolean|string`, `number|string` and `any`.\\n\\n### Multiple `.env` files support\\n\\nVersion 2 allows you to use different `.env` files depending on your environment.\\n\\nIf you configuration is as follows and `NODE_ENV` equals `production`, then the framework will look at `.env.production` to retrieve the value and if it does not exist (the file or the value), Foal will look at `.env`.\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n secret: env(SETTINGS_JWT_SECRET)\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"secret\\": \\"env(SETTINGS_JWT_SECRET)\\",\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nconst { Env } = require(\'@foal/core\');\\n\\nmodule.exports = {\\n settings: {\\n jwt: {\\n secret: Env.get(\'SETTINGS_JWT_SECRET\')\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n\\n### Three config formats (JS, JSON, YAML)\\n\\nJSON and YAML were already supported in version 1. Starting from version 2, JS is also allowed.\\n\\n\\n\\n\\n```yaml\\nsettings:\\n session:\\n store: \\"@foal/typeorm\\"\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"session\\": {\\n \\"store\\": \\"@foal/typeorm\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n session: {\\n store: \\"@foal/typeorm\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n### More Liberty in Naming Environment Variables\\n\\nIn version 1, the names of the environment variables were depending on the names of the configuration keys. For example, when using `Config.get(\'settings.mongodbUri\')`, Foal was looking at `SETTINGS_MONGODB_URI`.\\n\\nStarting from version 2, it is your responsability to choose the environement variable that you want to use (if you use one). This gives more flexibility especially when a Cloud provider defines its own variable names.\\n\\n\\n\\n\\n```yaml\\nsettings:\\n mongodbUri: env(MONGODB_URI)\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"mongodbUri\\": \\"env(MONGODB_URI)\\"\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nconst { Env } = require(\'@foal/core\');\\n\\nmodule.exports = {\\n settings: {\\n mongodbUri: Env.get(\'MONGODB_URI\')\\n }\\n}\\n```\\n\\n\\n\\n\\n## OpenAPI Schemas & Validation\\n\\nStarting from version 1, Foal has allowed you to generate a complete [Swagger](https://swagger.io/tools/swagger-ui/) interface by reading your code. If your application has validation and auth hooks for example, Foal will use them to generate the proper interface.\\n\\nThis is a handy if you want to quickly test and document your API. Then you can customize it in your own way if you wish and complete and override the OpenAPI spec generated by the framework.\\n\\nIn version 2, support of Swagger has been increased to allow you to define OpenAPI schemas and re-use them for validation.\\n\\nHere is an example.\\n\\n*product.controller.ts*\\n```typescript\\nimport { ApiDefineSchema, ApiResponse, Context, Get, HttpResponseNotFound, HttpResponseOK, Post, ValidateBody, ValidatePathParam } from \'@foal/core\';\\nimport { Product } from \'../../entities\';\\n\\n// First we define the OpenAPI schema \\"Product\\".\\n@ApiDefineSchema(\'Product\', {\\n type: \'object\',\\n properties: {\\n id: { type: \'number\' },\\n name: { type: \'string\' }\\n },\\n additionalProperties: false,\\n required: [\'id\', \'name\'],\\n})\\nexport class ProductController {\\n\\n @Post(\'/\')\\n // We use the schema \\"Product\\" here to validate the request body.\\n @ValidateBody({ $ref: \'#/components/schemas/Product\' })\\n async createProduct(ctx: Context) {\\n const result = await Product.insert(ctx.request.body);\\n return new HttpResponseOK(result.identifiers[0]);\\n }\\n\\n @Get(\'/:productId\')\\n // We use the schema \\"Product\\" here to validate the URL parameter.\\n @ValidatePathParam(\'productId\', { $ref: \'#/components/schemas/Product/properties/id\' })\\n // We give some extra information on the format of the response.\\n @ApiResponse(200, {\\n description: \'Product found in the database\',\\n content: {\\n \'application/json\': { schema: { $ref: \'#/components/schemas/Product\' } }\\n }\\n })\\n async readProduct(ctx: Context, { productId }) {\\n const product = await Product.findOne({ id: productId });\\n\\n if (!product) {\\n return new HttpResponseNotFound();\\n }\\n\\n return new HttpResponseOK(product);\\n }\\n\\n}\\n\\n```\\n\\n*api.controller.ts*\\n```typescript\\nimport { ApiInfo, ApiServer, Context, controller, Get, HttpResponseOK } from \'@foal/core\';\\nimport { ProductController } from \'./api\';\\n\\n// We provide the \\"info\\" metadata to describe the API.\\n@ApiInfo({\\n title: \'My API\',\\n version: \'0.1.0\'\\n})\\n@ApiServer({\\n url: \'/api\'\\n})\\nexport class ApiController {\\n subControllers = [\\n controller(\'/products\', ProductController)\\n ];\\n \\n}\\n```\\n\\n*openapi.controller.ts*\\n```typescript\\nimport { SwaggerController } from \'@foal/swagger\';\\nimport { ApiController } from \'./api.controller\';\\n\\n// This controller generates the Swagger interface.\\nexport class OpenapiController extends SwaggerController {\\n\\n options = {\\n controllerClass: ApiController,\\n }\\n\\n}\\n\\n```\\n\\n*app.controller.ts*\\n```typescript\\nimport { controller, IAppController } from \'@foal/core\';\\nimport { createConnection } from \'typeorm\';\\n\\nimport { ApiController, OpenapiController } from \'./controllers\';\\n\\nexport class AppController implements IAppController {\\n subControllers = [\\n controller(\'/api\', ApiController),\\n controller(\'/swagger\', OpenapiController),\\n ];\\n\\n async init() {\\n await createConnection();\\n }\\n}\\n\\n```\\n\\n![Swagger 1](./assets/whats-new-in-version-2-part-2/swagger.png)\\n\\n![Swagger 2](./assets/whats-new-in-version-2-part-2/swagger2.png)\\n\\n![Swagger 3](./assets/whats-new-in-version-2-part-2/swagger3.png)"},{"id":"/2021/02/25/version-2.2-release-notes","metadata":{"permalink":"/blog/2021/02/25/version-2.2-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-02-25-version-2.2-release-notes.md","source":"@site/blog/2021-02-25-version-2.2-release-notes.md","title":"Version 2.2 release notes","description":"Banner","date":"2021-02-25T00:00:00.000Z","formattedDate":"February 25, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.955,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.2 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.2-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 2/4)","permalink":"/blog/2021/03/02/whats-new-in-version-2-part-2"},"nextItem":{"title":"What\'s new in version 2 (part 1/4)","permalink":"/blog/2021/02/17/whats-new-in-version-2-part-1"}},"content":"![Banner](./assets/version-2.2-is-here/banner.png)\\n\\nVersion 2.2 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## New Look of the `createapp` Command\\n\\nThe output of the `createapp` command has been prettified to be more \\"welcoming\\".\\n\\n![New createapp look](./assets/version-2.2-is-here/new-create-app.png)\\n\\n## Authentication Improvement for Single-Page Applications (SPA)\\n\\nWhen building a SPA with cookie-based authentication, it can sometimes be difficult to know if the user is logged in or to obtain certain information about the user (`isAdmin`, etc).\\n\\nSince the authentication token is stored in a cookie with the `httpOnly` directive set to `true` (to mitigate XSS attacks), the front-end application has no way of knowing if a user is logged in, except by making an additional request to the server.\\n\\nTo solve this problem, version 2.2 adds a new option called `userCookie` that allows you to set an additional cookie that the frontend can read with the content you choose. This cookie is synchronized with the session and is refreshed at each request and destroyed when the session expires or when the user logs out.\\n\\nIn the following example, the `user` cookie is empty if no user is logged in or contains certain information about him/her otherwise. This is particularly useful if you need to display UI elements based on user characteristics.\\n\\n*Server-side code*\\n\\n```typescript\\nfunction userToJSON(user: User|undefined) {\\n if (!user) {\\n return \'null\';\\n }\\n\\n return JSON.stringify({\\n email: user.email,\\n isAdmin: user.isAdmin\\n });\\n}\\n\\n@UseSessions({\\n cookie: true,\\n user: fetchUser(User),\\n userCookie: (ctx, services) => userToJSON(ctx.user)\\n})\\nexport class ApiController {\\n\\n @Get(\'/products\')\\n @UserRequired()\\n async readProducts(ctx: Context) {\\n const products = await Product.find({ owner: ctx.user });\\n return new HttpResponseOK(products);\\n }\\n\\n}\\n```\\n\\n*Cookies*\\n\\n![User cookie](./assets/version-2.2-is-here/user-cookie.png)\\n\\n*Client-side code*\\n\\n```javascript\\nconst user = JSON.parse(decodeURIComponent(/* cookie value */));\\n```\\n\\n## Support of Nested Routes in `foal generate|g rest-api `\\n\\nLike the command `g controller`, `g rest-api` now supports nested routes.\\n\\nLet\'s say we have the following file structure:\\n\\n```\\nsrc/\\n \'- app/\\n |- controllers/\\n | |- api.controller.ts\\n | \'- index.ts\\n \'- entities/\\n |- user.entity.ts\\n \'- index.ts\\n```\\n\\nRunning these commands will add and register the following files:\\n\\n```\\nfoal generate rest-api api/product --auth --register\\nfoal generate rest-api api/order --auth --register\\n```\\n\\n```\\nsrc/\\n \'- app/\\n |- controllers/\\n | |- api/\\n | | |- product.controller.ts\\n | | |- order.controller.ts\\n | | \'- index.ts\\n | |- api.controller.ts\\n | \'- index.ts\\n \'- entities/\\n |- product.entity.ts\\n |- order.entity.ts\\n |- user.entity.ts\\n \'- index.ts\\n```"},{"id":"/2021/02/17/whats-new-in-version-2-part-1","metadata":{"permalink":"/blog/2021/02/17/whats-new-in-version-2-part-1","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-02-17-whats-new-in-version-2-part-1.md","source":"@site/blog/2021-02-17-whats-new-in-version-2-part-1.md","title":"What\'s new in version 2 (part 1/4)","description":"Banner","date":"2021-02-17T00:00:00.000Z","formattedDate":"February 17, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":4.69,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 1/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-1.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.2 release notes","permalink":"/blog/2021/02/25/version-2.2-release-notes"},"nextItem":{"title":"Version 2.1 release notes","permalink":"/blog/2021/02/03/version-2.1-release-notes"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-1/banner.png)\\n\\nThis article presents some improvements introduced in version 2 of FoalTS:\\n- the new CLI commands\\n- the service and application initialization\\n- the `AppController` interface\\n- custom error-handling & hook post functions\\n- accessing file metadata during uploads\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 1 of the series of articles *What\'s new in version 2.0*. Part 2 can be found [here](./2021-03-02-whats-new-in-version-2-part-2.md).\\n\\n## New CLI commands\\n\\nIn version 1, there were many commands to use, and this, in a specific order. Running and generating migrations from model changes required four commands and building the whole application needed three.\\n\\nIn version 2, the number of CLI commands has been reduced and they have been simplified so that one action matches one command.\\n\\n### Generating migrations\\n\\nThis command generates migrations by comparing the current database schema and the latest changes in your models.\\n\\n\\n\\n\\n```bash\\nnpm run makemigrations\\n```\\n\\n\\n\\n\\n```bash\\nnpm run build:app\\nnpm run migration:generate -- -n my_migration\\n```\\n\\n\\n\\n\\n\\n### Running migrations\\n\\nThis command builds and runs all migrations.\\n\\n\\n\\n\\n```bash\\nnpm run migrations\\n```\\n\\n\\n\\n\\n```bash\\nnpm run build:migrations\\nnpm run migration:run\\n```\\n\\n\\n\\n\\n### Build and run scripts in watch mode (development)\\n\\nIf you want to re-build your scripts each time a file is change, you can execute `npm run develop` in a separate terminal.\\n\\n\\n\\n\\n```bash\\n# In one terminal:\\nnpm run develop\\n\\n# In another terminal:\\nfoal run my-script\\n```\\n\\n\\n\\n\\n```bash\\n# In one terminal:\\nnpm run build:scripts:w\\n\\n# In another terminal:\\nfoal run my-script\\n```\\n\\n\\n\\n\\n### Revert one migration\\n\\nThis command reverts the last executed migration.\\n\\n\\n\\n\\n```bash\\nnpm run revertmigration\\n```\\n\\n\\n\\n\\n```bash\\nnpm run migration:revert\\n```\\n\\n\\n\\n\\n### Build migrations, scripts and the app\\n\\nThis command builds the application, the scripts and the migrations. Unit and e2e tests are not included.\\n\\n\\n\\n\\n```bash\\nnpm run build\\n```\\n\\n\\n\\n\\n```bash\\nnpm run build:app\\nnpm run build:scripts\\nnpm run build:migrations\\n```\\n\\n\\n\\n\\n## Service and Application Initialization\\n\\nIn version 1, it was possible to add an `init` method to the `AppController` class and `boot` methods in the services to initialize the application. These features needed special options in order to be activated.\\n\\nStarting from version 2, they are enabled by default.\\n\\n```typescript\\nexport class AppController {\\n // ...\\n\\n init() {\\n // Execute some code.\\n }\\n}\\n```\\n\\n```typescript\\nexport class MyService {\\n // ...\\n\\n boot() {\\n // Execute some code.\\n }\\n}\\n```\\n\\n## The `AppController` interface\\n\\nThis optional interface allows you to check that the `subControllers` property has the correct type as well as the `init` and `handleError` methods.\\n\\n```typescript\\nexport class AppController implements IAppController {\\n subControllers = [\\n controller(\'/api\', ApiController)\\n ];\\n\\n init() {\\n // ...\\n }\\n\\n handleError(error, ctx) {\\n // ...\\n }\\n}\\n```\\n\\n## Custom Error-Handling & Hook Post Functions\\n\\nIn version 1, when an error was thrown or rejected in a hook or a controller method, the remaining hook post functions were not executed.\\n\\nStarting from version 2, the error is directly converted to an `HttpResponseInternalServerError` and passed to the next post hook functions.\\n\\nThis can be useful in case we want to use exceptions as HTTP responses without breaking the hook post functions.\\n\\n*Example*\\n```typescript\\nclass PermissionError extends Error {}\\n\\nclass UserService {\\n\\n async listUsers(applicant: User): Promise {\\n if (!ctx.user.isAdmin) {\\n // Use exception here.\\n throw new PermissionError();\\n }\\n\\n return User.find({ org: user.org });\\n }\\n\\n}\\n\\n// This hook measures the execution time and the controller method and hooks.\\n@Hook(() => {\\n const time = process.hrtime();\\n\\n // This post function will still be executed\\n // even if an error is thrown in listUsers.\\n return () => {\\n const seconds = process.hrtime(time)[0];\\n console.log(`Executed in ${seconds} seconds`);\\n };\\n})\\nexport class AppController {\\n\\n @dependency\\n users: UserService;\\n\\n @Get(\'/users\')\\n @UseSessions({ user: fetchUser(User) })\\n @UserRequired()\\n listUsers(ctx: Context) {\\n return new HttpResponseOK(\\n await users.listUsers(ctx.user)\\n );\\n }\\n\\n handleError(error: Error, ctx: Context) {\\n // Converts the exception to an HTTP response.\\n // The error can have been thrown in a service used by the controller.\\n if (error instanceof PermissionError) {\\n return new HttpResponseForbidden();\\n }\\n\\n // Returns an HttpResponseInternalServerError.\\n return renderError(error, response);\\n }\\n}\\n```\\n\\n## Accessing File Metadata during Uploads\\n\\nWhen using the `@ValidateMultipartFormDataBody` hook to handle file upload, it is now possible to access the file metadata.\\n\\n*Example*\\n```typescript\\nexport class UserController {\\n\\n @Post(\'/profile\')\\n @ValidateMultipartFormDataBody({\\n files: {\\n profile: { required: true },\\n }\\n })\\n uploadProfilePhoto(ctx: Context) {\\n const file = ctx.request.body.files.profile;\\n // file.mimeType, file.buffer\\n }\\n\\n}\\n```\\n\\n| Property name | Type | Description |\\n| --- | --- | --- |\\n| `encoding` | `string` | Encoding type of the file |\\n| `filename` | `string\\\\|undefined` | Name of the file on the user\'s computer |\\n| `mimeType` | `string` | Mime type of the file |\\n| `path` | `string` | Path where the file has been saved. If the `saveTo` option was not provided, the value is an empty string. |\\n| `buffer` | `Buffer` | Buffer containing the entire file. If the `saveTo` option was provided, the value is an empty buffer. |"},{"id":"/2021/02/03/version-2.1-release-notes","metadata":{"permalink":"/blog/2021/02/03/version-2.1-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-02-03-version-2.1-release-notes.md","source":"@site/blog/2021-02-03-version-2.1-release-notes.md","title":"Version 2.1 release notes","description":"Banner","date":"2021-02-03T00:00:00.000Z","formattedDate":"February 3, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.495,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.1 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 1/4)","permalink":"/blog/2021/02/17/whats-new-in-version-2-part-1"}},"content":"![Banner](./assets/version-2.1-is-here/banner.png)\\n\\nVersion 2.1 has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## New Error Page Design\\n\\nWhen an error is thrown or rejected in development, the server returns an error page with some debugging details. The UI of this page has been improved and it now provides more information.\\n\\n![Error page](./assets/version-2.1-is-here/error-page.png)\\n\\n## New Welcome Page\\n\\nWhen creating a new project, the generated welcome page is also different.\\n\\n![Welcome page](./assets/version-2.1-is-here/welcome-page.png)\\n\\n## CLI exits with code 1 when a command fails\\n\\nThis small improvement is useful when we want to stop a CI pipeline when one of its commands fails.\\n\\n## New `@All` decorator\\n\\nThis decorator handles all requests regardless of the HTTP verb (GET, POST, etc.).\\n\\nIt can be used for example to create a `not found` handler.\\n\\n```typescript\\nimport { All, HttpResponseNotFound } from \'@foal/core\';\\n\\nclass AppController {\\n subControllers = [ ViewController ];\\n\\n @All(\'*\')\\n notFound() {\\n return new HttpResponseNotFound(\'The route you are looking for does not exist.\');\\n }\\n}\\n```\\n\\n## New CSRF option in `@UseSessions` and `@JWT`\\n\\nThis option allows you to override the behavior of the configuration specified globally with the key `settings.session.csrf.enabled` or the key `settings.jwt.csrf.enabled`.\\n\\nIt can be useful for example to disable the CSRF protection on a specific route.\\n\\n```typescript\\nimport { HttpResponseOK, Post, UseSessions } from \'@foal/core\';\\n\\nexport class ApiController {\\n @Post(\'/foo\')\\n @UseSessions({ cookie: true })\\n foo() {\\n // This method has the CSRF protection enabled.\\n return new HttpResponseOK();\\n }\\n\\n @Post(\'/bar\')\\n @UseSessions({ cookie: true, csrf: false })\\n bar() {\\n // This method does not have the CSRF protection enabled.\\n return new HttpResponseOK();\\n }\\n}\\n\\n```\\n\\n## Support of `better-sqlite3`\\n\\nWhen using Foal with SQLite, you now have the choice between two drivers: `sqlite3` and `better-sqlite3`. The package `better-sqlite3` is used by default in new projects starting from this version on."}]}')}}]); \ No newline at end of file diff --git a/assets/js/b2f554cd.ebe0ede0.js b/assets/js/b2f554cd.ebe0ede0.js deleted file mode 100644 index 7e912de75b..0000000000 --- a/assets/js/b2f554cd.ebe0ede0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5894],{76042:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"/2024/04/25/version-4.4-release-notes","metadata":{"permalink":"/blog/2024/04/25/version-4.4-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-25-version-4.4-release-notes.md","source":"@site/blog/2024-04-25-version-4.4-release-notes.md","title":"Version 4.4 release notes","description":"Banner","date":"2024-04-25T00:00:00.000Z","formattedDate":"April 25, 2024","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.19,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.4 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.4-release-notes.png","tags":["release"]},"unlisted":false,"nextItem":{"title":"Version 4.3 release notes","permalink":"/blog/2024/04/16/version-4.3-release-notes"}},"content":"![Banner](./assets/version-4.4-is-here/banner.png)\\n\\nVersion 4.4 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\nThis release updates Foal\'s sub-dependencies, including the `express` library, which presents a moderate vulnerability in versions prior to 4.19.2.\\n\\nThanks to [Lucho](https://github.com/lcnvdl) for reporting this vulnerability in the first place!"},{"id":"/2024/04/16/version-4.3-release-notes","metadata":{"permalink":"/blog/2024/04/16/version-4.3-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-16-version-4.3-release-notes.md","source":"@site/blog/2024-04-16-version-4.3-release-notes.md","title":"Version 4.3 release notes","description":"Banner","date":"2024-04-16T00:00:00.000Z","formattedDate":"April 16, 2024","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.125,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.3 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.3-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.4 release notes","permalink":"/blog/2024/04/25/version-4.4-release-notes"},"nextItem":{"title":"Version 4.2 release notes","permalink":"/blog/2023/10/29/version-4.2-release-notes"}},"content":"![Banner](./assets/version-4.3-is-here/banner.png)\\n\\nVersion 4.3 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better CLI ouput when script arguments are invalid\\n\\nPreviously, when executing `foal run my-script` with invalid arguments, the CLI would only display one error at a time.\\n\\nFor example, with the following schema and arguments, we would only get this error message:\\n\\n```typescript\\nexport const schema = {\\n type: \'object\', \\n properties: {\\n email: { type: \'string\', format: \'email\', maxLength: 2 },\\n password: { type: \'string\' },\\n n: { type: \'number\', maximum: 10 }\\n },\\n required: [\'password\']\\n};\\n```\\n\\n```bash\\nfoal run my-script email=bar n=11\\n```\\n\\n```\\nError: The command line arguments must match format \\"email\\".\\n```\\n\\nFrom version 4.3 onwards, the CLI logs all validation errors and with a more meaningful description.\\n\\n```\\nScript error: arguments must have required property \'password\'.\\nScript error: the value of \\"email\\" must NOT have more than 2 characters.\\nScript error: the value of \\"email\\" must match format \\"email\\".\\nScript error: the value of \\"n\\" must be <= 10.\\n```\\n\\n## [Fix] the logger no longer throws an error in development when the client request is interrupted\\n\\nUsing the logger\'s `dev` format, Foal would occasionally throw the error `TypeError: Cannot read properties of null`.\\n\\nThis would occur when the connection with the client was lost, which happens, for example, when the React client server hotly reloads.\\n\\nThis version fixes this error."},{"id":"/2023/10/29/version-4.2-release-notes","metadata":{"permalink":"/blog/2023/10/29/version-4.2-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-10-29-version-4.2-release-notes.md","source":"@site/blog/2023-10-29-version-4.2-release-notes.md","title":"Version 4.2 release notes","description":"Banner","date":"2023-10-29T00:00:00.000Z","formattedDate":"October 29, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.615,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.2 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.2-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.3 release notes","permalink":"/blog/2024/04/16/version-4.3-release-notes"},"nextItem":{"title":"Version 4.1 release notes","permalink":"/blog/2023/10/24/version-4.1-release-notes"}},"content":"![Banner](./assets/version-4.2-is-here/banner.png)\\n\\nVersion 4.2 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better logging for socket.io controllers\\n\\nSocket.io messages are now logged in the same way as HTTP requests.\\n\\n## AJV strict mode can be disabled\\n\\nAJV [strict mode](https://ajv.js.org/strict-mode.html) can be disabled thanks to the new config key `settings.ajv.strict`:\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"ajv\\": {\\n \\"strict\\": false\\n }\\n }\\n}\\n```\\n\\n## `foal connect angular` command fixed\\n\\nThe command that allows to set up a project with Angular and Foal has been fixed to support the latest versions of Angular. \\n\\n## Cache control can be disabled for static files\\n\\nThe `cacheControl` option of the `express.static` middleware can be passed through the configuration.\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"staticFiles\\": {\\n \\"cacheControl\\": false\\n }\\n }\\n}\\n```"},{"id":"/2023/10/24/version-4.1-release-notes","metadata":{"permalink":"/blog/2023/10/24/version-4.1-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-10-24-version-4.1-release-notes.md","source":"@site/blog/2023-10-24-version-4.1-release-notes.md","title":"Version 4.1 release notes","description":"Banner","date":"2023-10-24T00:00:00.000Z","formattedDate":"October 24, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.625,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.1 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.1-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.2 release notes","permalink":"/blog/2023/10/29/version-4.2-release-notes"},"nextItem":{"title":"Version 4.0 release notes","permalink":"/blog/2023/09/11/version-4.0-release-notes"}},"content":"![Banner](./assets/version-4.1-is-here/banner.png)\\n\\nVersion 4.1 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better logging\\n\\nFoal now features a true logging system. Full documentation can be found [here](/docs/common/logging).\\n\\n### New recommended configuration\\n\\nIt is recommended to switch to this configuration to take full advantage of the new logging system.\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"loggerFormat\\": \\"foal\\"\\n }\\n}\\n```\\n\\n*config/development.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"logger\\": {\\n \\"format\\": \\"dev\\"\\n }\\n }\\n}\\n```\\n\\n## Request IDs\\n\\nOn each request, a request ID is now generated randomly. It can be read through `ctx.request.id`.\\n\\nIf the `X-Request-ID` header exists in the request, then the header value is used as the request identifier.\\n\\n## XML requests\\n\\nIf a request is sent with the `application/xml` header, the XML content is now available under `ctx.request.body`."},{"id":"/2023/09/11/version-4.0-release-notes","metadata":{"permalink":"/blog/2023/09/11/version-4.0-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-09-11-version-4.0-release-notes.md","source":"@site/blog/2023-09-11-version-4.0-release-notes.md","title":"Version 4.0 release notes","description":"Banner","date":"2023-09-11T00:00:00.000Z","formattedDate":"September 11, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.005,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 4.0 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-4.0-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.1 release notes","permalink":"/blog/2023/10/24/version-4.1-release-notes"},"nextItem":{"title":"Version 3.3 release notes","permalink":"/blog/2023/08/13/version-3.3-release-notes"}},"content":"![Banner](./assets/version-4.0-is-here/banner.png)\\n\\nVersion 4.0 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Description\\n\\nThe goals of this major release are to\\n- support the latest active versions of Node (18 and 20) and update all internal dependencies to their latest major versions\\n- facilitate framework maintenance.\\n\\nFull details can be found [here](https://github.com/FoalTS/foal/issues/1223).\\n\\n## Migration guide\\n\\n- Run `npx foal upgrade`.\\n- Version 16 of Node is not supported anymore. Upgrade to version 18 or version 20.\\n- Support of MariaDB has been dropped.\\n- If you use any of these dependencies, upgrade `typeorm` to v0.3.17, `graphql` to v16, `type-graphql` to v2, `class-validator` to v0.14, `mongodb` to v5 and `@socket.io/redis-adapter` to v8.\\n- If you use both TypeORM and `MongoDBStore`, there is no need anymore to maintain two versions of `mongodb`. You can use version 5 of `mongodb` dependency.\\n- If you use `@foal/socket.io` with redis, install `socket.io-adapter`.\\n- Support for `better-sqlite` driver has been dropped. Use the `sqlite3` driver instead. In DB configuration, use `type: \'sqlite\'` instead of `type: \'better-sqlite3\'`.\\n- In your project dependencies, upgrade `@types/node` to v18.11.9.\\n- If you use TypeORM with MongoDB, for the entities definitions, rename `import { ObjectID } from \'typeorm\';` to `import { ObjectId } from \'typeorm\';`"},{"id":"/2023/08/13/version-3.3-release-notes","metadata":{"permalink":"/blog/2023/08/13/version-3.3-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-08-13-version-3.3-release-notes.md","source":"@site/blog/2023-08-13-version-3.3-release-notes.md","title":"Version 3.3 release notes","description":"Banner","date":"2023-08-13T00:00:00.000Z","formattedDate":"August 13, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.265,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.3 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.3-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 4.0 release notes","permalink":"/blog/2023/09/11/version-4.0-release-notes"},"nextItem":{"title":"Version 3.2 release notes","permalink":"/blog/2023/04/04/version-3.2-release-notes"}},"content":"![Banner](./assets/version-3.3-is-here/banner.png)\\n\\nVersion 3.3 of [Foal](https://foalts.org/) is out!\\n\\n\x3c!--truncate--\x3e\\n\\n## Better security for JWT\\n\\nThe `jsonwebtoken` dependency has been upgraded to v9 to address [security issues](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md#900---2022-12-21).\\n\\n> Note that RSA key size now must be 2048 bits or greater. Make sure to check the size of your RSA key before upgrading to this version."},{"id":"/2023/04/04/version-3.2-release-notes","metadata":{"permalink":"/blog/2023/04/04/version-3.2-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2023-04-04-version-3.2-release-notes.md","source":"@site/blog/2023-04-04-version-3.2-release-notes.md","title":"Version 3.2 release notes","description":"Banner","date":"2023-04-04T00:00:00.000Z","formattedDate":"April 4, 2023","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.54,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.2 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.2-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.3 release notes","permalink":"/blog/2023/08/13/version-3.3-release-notes"},"nextItem":{"title":"Version 3.1 release notes","permalink":"/blog/2022/11/28/version-3.1-release-notes"}},"content":"![Banner](./assets/version-3.2-is-here/banner.png)\\n\\nVersion 3.2 of [Foal](https://foalts.org/) is out! Here are the improvements that it brings:\\n\\n\x3c!--truncate--\x3e\\n\\n## New package `@foal/password`\\n\\nThe `foal/password` package, which was removed in v3.0, has been re-added. It offers an `isCommon` method to check if a password is too common:\\n\\n```typescript\\nconst isPasswordTooCommon = await isCommon(password);\\n```\\n\\n## Read the controller and the controller method names in request contexts\\n\\nThe `Context` and `WebsocketContext` have two new properties:\\n\\n\\n | Name | Type | Description |\\n | --- | --- | --- |\\n | `controllerName` | `string` | The name of the controller class. |\\n | `controllerMethodName` | `string` | The name of the controller method. |"},{"id":"/2022/11/28/version-3.1-release-notes","metadata":{"permalink":"/blog/2022/11/28/version-3.1-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-11-28-version-3.1-release-notes.md","source":"@site/blog/2022-11-28-version-3.1-release-notes.md","title":"Version 3.1 release notes","description":"Banner","date":"2022-11-28T00:00:00.000Z","formattedDate":"November 28, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.765,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.1 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.1-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.2 release notes","permalink":"/blog/2023/04/04/version-3.2-release-notes"},"nextItem":{"title":"Version 3.0 release notes","permalink":"/blog/2022/11/01/version-3.0-release-notes"}},"content":"![Banner](./assets/version-3.1-is-here/banner.png)\\n\\nVersion 3.1 of [Foal](https://foalts.org/) is out! Here are the improvements that it brings:\\n\\n\x3c!--truncate--\x3e\\n\\n## New `foal upgrade` command\\n\\nThis command allows you to upgrade all `@foal/*` dependencies and dev dependencies to a given version.\\n\\n*Examples*\\n```bash\\nfoal upgrade # upgrade to the latest version\\nfoal upgrade 3.0.0\\nfoal upgrade \\"~3.0.0\\"\\n```\\n\\n## Social authentication supports subdomains\\n\\nIf you\'re using multiple subdomains domains to handle social authentication, you can now do so by specifying a custom cookie domain in the configuration:\\n\\n```yaml\\nsettings:\\n social:\\n cookie:\\n domain: foalts.org\\n```\\n\\n## Regression on OpenAPI keyword \\"example\\" has been fixed\\n\\nIn version 3.0, using the keyword `example` in an validation object was raising an error. This has been fixed.\\n\\n## `.env` files support whitespaces\\n\\nWhitespaces around the equal symbol are now allowed:\\n\\n```bash\\nFOO_BAR_WITH_WHITESPACES_AROUND_THE_NAME = hello you\\n```\\n\\n## Value of the `Strict-Transport-Security` header has been increased\\n\\nIt has been increased from 15,552,000 to 31,536,000."},{"id":"/2022/11/01/version-3.0-release-notes","metadata":{"permalink":"/blog/2022/11/01/version-3.0-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-11-01-version-3.0-release-notes.md","source":"@site/blog/2022-11-01-version-3.0-release-notes.md","title":"Version 3.0 release notes","description":"Banner","date":"2022-11-01T00:00:00.000Z","formattedDate":"November 1, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":6.72,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 3.0 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-3.0-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.1 release notes","permalink":"/blog/2022/11/28/version-3.1-release-notes"},"nextItem":{"title":"Version 2.11 release notes","permalink":"/blog/2022/10/09/version-2.11-release-notes"}},"content":"![Banner](./assets/version-3.0-is-here/banner.png)\\n\\nVersion 3.0 of [Foal](https://foalts.org/) is finally there!\\n\\nIt\'s been a long work and I\'m excited to share with you the new features of the framework \ud83c\udf89 . The upgrading guide can be found [here](https://foalts.org/docs/3.x/upgrade-to-v3/).\\n\\nHere are the new features and improvements of version 3!\\n\\n\x3c!--truncate--\x3e\\n\\n## Full support of TypeORM v0.3\\n\\n> For those new to Foal, TypeORM is the default ORM used in all new projects. But you can use any other ORM or query builder if you want, as the core framework is ORM independent.\\n\\nTypeORM v0.3 provides greater typing safety and this is something that will be appreciated when moving to the new version of Foal.\\n\\nThe version 0.3 of TypeORM has a lot of changes compared to the version 0.2 though. Features such as the `ormconfig.json` file have been removed and functions such as `createConnection`, `getManager` or `getRepository` have been deprecated.\\n\\nA lot of work has been done to make sure that `@foal/typeorm`, new projects generated by the CLI and examples in the documentation use version 0.3 of TypeORM without relying on deprecated functions or patterns.\\n\\nIn particular, the connection to the database is now managed by a file `src/db.ts` that replaces the older `ormconfig.json`.\\n\\n## Code simplified\\n\\nSome parts of the framework have been simplified to require less code and make it more understandable.\\n\\n### Authentication\\n\\nThe `@UseSessions` and `@JWTRequired` authentication hooks called obscure functions such as `fetchUser`, `fetchUserWithPermissions` to populate the `ctx.user` property. The real role of these functions was not clear and a newcomer to the framework could wonder what they were for.\\n\\nThis is why these functions have been removed and replaced by direct calls to database models.\\n\\n```typescript\\n// Version 2\\n@UseSessions({ user: fetchUser(User) })\\n@JWTRequired({ user: fetchUserWithPermissions(User) })\\n\\n// Version 3\\n@UseSessions({ user: (id: number) => User.findOneBy({ id }) })\\n@JWTRequired({ user: (id: number) => User.findOneWithPermissionsBy({ id }) })\\n```\\n\\n### File upload\\n\\nWhen uploading files in a _multipart/form-data_ request, it was not allowed to pass optional fields. This is now possible. \\n\\nThe interface of the `@ValidateMultipartFormDataBody` hook, renamed to `@ParseAndValidateFiles` to be more understandable for people who don\'t know the HTTP protocol handling the upload, has been simplified.\\n\\n*Examples with only files*\\n```typescript\\n// Version 2\\n@ValidateMultipartFormDataBody({\\n files: {\\n profile: { required: true }\\n }\\n})\\n\\n// Version 3\\n@ParseAndValidateFiles({\\n profile: { required: true }\\n})\\n```\\n\\n*Examples with files and fields*\\n```typescript\\n// Version 2\\n@ValidateMultipartFormDataBody({\\n files: {\\n profile: { required: true }\\n }\\n fields: {\\n description: { type: \'string\' }\\n }\\n})\\n\\n// Version 3\\n@ParseAndValidateFiles(\\n {\\n profile: { required: true }\\n },\\n // The second parameter is optional\\n // and is used to add fields. It expects an AJV object.\\n {\\n type: \'object\',\\n properties: {\\n description: { type: \'string\' }\\n },\\n required: [\'description\'],\\n additionalProperties: false\\n }\\n)\\n```\\n\\n### Database models\\n\\nUsing functions like `getRepository` or `getManager` to manipulate data in a database is not necessarily obvious to newcomers. It adds complexity that is not necessary for small or medium sized projects. Most frameworks prefer to use the Active Record pattern for simplicity.\\n\\nThis is why, from version 3 and to take into account that TypeORM v0.3 no longer uses a global connection, the examples in the documentation and the generators will extend all the models from `BaseEntity`. Of course, it will still be possible to use the functions below if desired. \\n\\n```typescript\\n// Version 2\\n@Entity()\\nclass User {}\\n\\nconst user = getRepository(User).create();\\nawait getRepository(User).save(user);\\n\\n// Version 3\\n@Entity()\\nclass User extends BaseEntity {}\\n\\nconst user = new User();\\nawait user.save();\\n```\\n\\n## Better typing\\n\\nThe use of TypeScript types has been improved and some parts of the framework ensure better type safety.\\n\\n### Validation with AJV\\n\\nFoal\'s version uses `ajv@8` which allows you to bind a TypeScript type with a JSON schema object. To do this, you can import the generic type `JSONSchemaType` to build the interface of the schema object.\\n\\n```typescript\\nimport { JSONSchemaType } from \'ajv\';\\n\\ninterface MyData {\\n foo: number;\\n bar?: string\\n}\\n\\nconst schema: JSONSchemaType = {\\n type: \'object\',\\n properties: {\\n foo: { type: \'integer\' },\\n bar: { type: \'string\', nullable: true }\\n },\\n required: [\'foo\'],\\n additionalProperties: false\\n}\\n```\\n\\n### File upload\\n\\nIn version 2, handling file uploads in the controller was tedious because all types were `any`. Starting with version 3, it is no longer necessary to cast the types to `File` or `File[]`:\\n\\n```typescript\\n// Version 2\\nconst name = ctx.request.body.fields.name;\\nconst file = ctx.request.body.files.avatar as File;\\nconst files = ctx.request.body.files.images as File[];\\n\\n// After\\nconst name = ctx.request.body.name;\\n// file is of type \\"File\\"\\nconst file = ctx.files.get(\'avatar\')[0];\\n// files is of type \\"Files\\"\\nconst files = ctx.files.get(\'images\');\\n```\\n\\n### Authentication\\n\\nIn version 2, the `user` option of `@UseSessions` and `@JWTRequired` expected a function with this signature:\\n\\n```typescript\\n(id: string|number, services: ServiceManager) => Promise;\\n```\\n\\nThere was no way to guess and guarantee the type of the user ID and the function had to check and convert the type itself if necessary.\\n\\nThe returned type was also very permissive (type `any`) preventing us from detecting silly errors such as confusion between `null` and `undefined` values.\\n\\nIn version 3, the hooks have been added a new `userIdType` option to check and convert the JavaScript type if necessary and force the TypeScript type of the function. The returned type is also safer and corresponds to the type of `ctx.user` which is no longer `any` but `{ [key : string] : any } | null`.\\n\\n*Example where the ID is a string*\\n```typescript\\n@JWTRequired({\\n user: (id: string) => User.findOneBy({ id });\\n userIdType: \'string\',\\n})\\n```\\n\\n*Example where the ID is a number*\\n```typescript\\n@JWTRequired({\\n user: (id: number) => User.findOneBy({ id });\\n userIdType: \'number\',\\n})\\n```\\n\\nBy default, the value of `userIdType` is a number, so we can simply write this: \\n\\n```typescript\\n@JWTRequired({\\n user: (id: number) => User.findOneBy({ id });\\n})\\n```\\n\\n### GraphQL\\n\\nIn version 2, GraphQL schemas were of type `any`. In version 3, they are all based on the `GraphQLSchema` interface.\\n\\n## Closer to JS ecosystem standards\\n\\nSome parts have been modified to get closer to the JS ecosystem standards. In particular:\\n\\n### Development command\\n\\nThe `npm run develop` has been renamed to `npm run dev`.\\n\\n### Configuration through environment variables\\n\\nWhen two values of the same variable are provided by a `.env` file and an environment variable, then the value of the environment is used (the behavior is similar to that of the [dotenv](https://www.npmjs.com/package/dotenv) library).\\n\\n### `null` vs `undefined` values\\n\\nWhen the request has no session or the user is not authenticated, the values of `ctx.session` and `ctx.user` are `null` and no longer `undefined`. This makes sense from a semantic point of view, and it also simplifies the user assignment from the `find` functions of popular ORMs (Prisma, TypeORM, Mikro-ORM). They all return `null` when no value is found.\\n\\n## More open to other ORMs\\n\\nTypeORM is the default ORM used in the documentation examples and in the projects generated by the CLI. But it is quite possible to use another ORM or query generator with Foal. For example, the authentication system (with sessions or JWT) makes no assumptions about database access.\\n\\nSome parts of the framework were still a bit tied to TypeORM in version 2. Version 3 fixed this.\\n\\n### Shell scripts\\n\\nWhen running the `foal generate script` command, the generated script file no longer contains TypeORM code.\\n\\n### Permission system\\n\\nThe `@PermissionRequired` option is no longer bound to TypeORM and can be used with any `ctx.user` that implements the `IUserWithPermissions` interface.\\n\\n## Smaller AWS S3 package\\n\\nThe `@foal/aws-s3` package is now based on version 3 of the AWS SDK. Thanks to this, the size of the `node_modules` has been reduced by three.\\n\\n## Dependencies updated and support of Node latest versions\\n\\nAll Foal\'s dependencies have been upgraded. The framework is also tested on Node versions 16 and 18.\\n\\n## Some bug fixes\\n\\nIf the configuration file `production.js` explicitly returns `undefined` for a given key and the `default.json` file returns a defined value for this key, then the value from the `default.json` file is returned by `Config.get`."},{"id":"/2022/10/09/version-2.11-release-notes","metadata":{"permalink":"/blog/2022/10/09/version-2.11-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-10-09-version-2.11-release-notes.md","source":"@site/blog/2022-10-09-version-2.11-release-notes.md","title":"Version 2.11 release notes","description":"Banner","date":"2022-10-09T00:00:00.000Z","formattedDate":"October 9, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.975,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.11 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.11-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 3.0 release notes","permalink":"/blog/2022/11/01/version-3.0-release-notes"},"nextItem":{"title":"Version 2.10 release notes","permalink":"/blog/2022/08/11/version-2.10-release-notes"}},"content":"![Banner](./assets/version-2.11-is-here/banner.png)\\n\\nVersion 2.11 of [Foal](https://foalts.org/) is out! Here are the improvements that it brings:\\n\\n\x3c!--truncate--\x3e\\n\\n## Number of Iterations on Password Hashing Has Been Increased\\n\\nThe PBKDF2 algorithm (used for password hashing) uses a number of iterations to hash passwords. This work factor is deliberate and slows down potential attackers, making attacks against hashed passwords more difficult.\\n\\nAs computing power increases, the number of iterations must also increase. This is why, starting with version 2.11, the number of iterations has been increased to 310,000.\\n\\nTo check that an existing password hash is using the latest recommended number of iterations, you can use the `passwordHashNeedsToBeRefreshed` function.\\n\\nThe example below shows how to perform this check during a login and how to upgrade the password hash if the number of iterations turns out to be too low.\\n\\n```typescript\\nconst { email, password } = ctx.request.body;\\n\\nconst user = await User.findOne({ email });\\n\\nif (!user) {\\n return new HttpResponseUnauthorized();\\n}\\n\\nif (!await verifyPassword(password, user.password)) {\\n return new HttpResponseUnauthorized();\\n}\\n\\n// highlight-start\\n// This line must be after the password verification.\\nif (passwordHashNeedsToBeRefreshed(user.password)) {\\n user.password = await hashPassword(password);\\n await user.save();\\n}\\n// highlight-end\\n\\n// Log the user in.\\n```"},{"id":"/2022/08/11/version-2.10-release-notes","metadata":{"permalink":"/blog/2022/08/11/version-2.10-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-08-11-version-2.10-release-notes.md","source":"@site/blog/2022-08-11-version-2.10-release-notes.md","title":"Version 2.10 release notes","description":"Banner","date":"2022-08-11T00:00:00.000Z","formattedDate":"August 11, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.695,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.10 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.10-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.11 release notes","permalink":"/blog/2022/10/09/version-2.11-release-notes"},"nextItem":{"title":"FoalTS 2022 survey is open!","permalink":"/blog/2022/06/13/FoalTS-2022-survey-is-open"}},"content":"![Banner](./assets/version-2.10-is-here/banner.png)\\n\\nVersion 2.10 of [Foal](https://foalts.org/) is out! This small release brings some tiny improvements.\\n\\n\x3c!--truncate--\x3e\\n\\n## `@foal/cli` package included by default as dev dependency\\n\\nIssue: [#1097](https://github.com/FoalTS/foal/issues/1097)\\n\\nThe `@foal/cli` package is now installed by default as dev dependency. In this way, all commands of `package.json` still work when deploying the application to a Cloud provider that does not have the CLI installed globally.\\n\\nContributor: [@scho-to](https://github.com/scho-to/)\\n\\n## Preventing the `npm run develop` command to get stuck on some OS\\n\\nIssues: [#1022](https://github.com/FoalTS/foal/issues/1022), [#1115](https://github.com/FoalTS/foal/issues/1115)\\n\\nThe `npm run develop` was getting stuck on some OS based on the configuration of the app. This issue is now fixed in new projects. For current applications, you will need to add a `-r` flag to the `package.json` commands using `concurrently`.\\n\\n## Smaller `main` function\\n\\nThe `main` function that bootstraps the application is now smaller in new projects."},{"id":"/2022/06/13/FoalTS-2022-survey-is-open","metadata":{"permalink":"/blog/2022/06/13/FoalTS-2022-survey-is-open","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-06-13-FoalTS-2022-survey-is-open.md","source":"@site/blog/2022-06-13-FoalTS-2022-survey-is-open.md","title":"FoalTS 2022 survey is open!","description":"FoalTS 2022 survey is now open (yes, a few months late \ud83d\ude43)!","date":"2022-06-13T00:00:00.000Z","formattedDate":"June 13, 2022","tags":[{"label":"survey","permalink":"/blog/tags/survey"}],"readingTime":0.325,"hasTruncateMarker":false,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"FoalTS 2022 survey is open!","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","tags":["survey"]},"unlisted":false,"prevItem":{"title":"Version 2.10 release notes","permalink":"/blog/2022/08/11/version-2.10-release-notes"},"nextItem":{"title":"Version 2.9 release notes","permalink":"/blog/2022/05/29/version-2.9-release-notes"}},"content":"FoalTS 2022 survey is now open (yes, a few months late \ud83d\ude43)!\\n\\nYour responses to these questions are really valuable as they help me better understand what you need and how to improve the framework going forward \ud83d\udc4c.\\n\\nI read every response carefully so feel free to say anything you have to say!\\n\\n\ud83d\udc49 [The link to the survey](https://forms.gle/3HAzQboxSBXvpJbB6).\\n\\nThe survey closes on June 31."},{"id":"/2022/05/29/version-2.9-release-notes","metadata":{"permalink":"/blog/2022/05/29/version-2.9-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-05-29-version-2.9-release-notes.md","source":"@site/blog/2022-05-29-version-2.9-release-notes.md","title":"Version 2.9 release notes","description":"Banner","date":"2022-05-29T00:00:00.000Z","formattedDate":"May 29, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.19,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.9 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.9-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"FoalTS 2022 survey is open!","permalink":"/blog/2022/06/13/FoalTS-2022-survey-is-open"},"nextItem":{"title":"Version 2.8 release notes","permalink":"/blog/2022/02/13/version-2.8-release-notes"}},"content":"![Banner](./assets/version-2.9-is-here/banner.png)\\n\\nVersion 2.9 of [Foal](https://foalts.org/) has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## New OAuth2 Twitter Provider\\n\\nAfter LinkedIn, Google, Github and Facebook, Foal now supports Twitter for social authentication.\\n\\n\ud83d\udc49 [Link to the documentation](https://foalts.org/docs/authentication/social-auth/)\\n\\nA big thanks to [@LeonardoSalvucci](https://github.com/LeonardoSalvucci) for having implemented this feature.\\n\\n```typescript\\n// 3p\\nimport { Context, dependency, Get } from \'@foal/core\';\\nimport { TwitterProvider } from \'@foal/social\';\\n\\nexport class AuthController {\\n @dependency\\n twitter: TwitterProvider;\\n\\n @Get(\'/signin/twitter\')\\n redirectToTwitter() {\\n // Your \\"Login In with Twitter\\" button should point to this route.\\n // The user will be redirected to Twitter auth page.\\n return this.twitter.redirect();\\n }\\n\\n @Get(\'/signin/twitter/callback\')\\n async handleTwitterRedirection(ctx: Context) {\\n // Once the user gives their permission to log in with Twitter, the OAuth server\\n // will redirect the user to this route. This route must match the redirect URI.\\n const { userInfo, tokens } = await this.twitter.getUserInfo(ctx);\\n\\n // Do something with the user information AND/OR the access token.\\n // If you only need the access token, you can call the \\"getTokens\\" method.\\n\\n // The method usually ends with a HttpResponseRedirect object as returned value.\\n }\\n\\n}\\n```\\n\\n## OAuth2 Providers support PKCE Code Flow\\n\\nOAuth2 abstract provider now supports PKCE code flow. If you wish to implement your own provider using PKCE, it\'s now possible!\\n\\n## Support for version 15 of `graphql` and latest version of `type-graphql`\\n\\nFoal\'s dependencies have been updated so as to support the latest version of [TypeGraphQL](https://typegraphql.com/)."},{"id":"/2022/02/13/version-2.8-release-notes","metadata":{"permalink":"/blog/2022/02/13/version-2.8-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2022-02-13-version-2.8-release-notes.md","source":"@site/blog/2022-02-13-version-2.8-release-notes.md","title":"Version 2.8 release notes","description":"Banner","date":"2022-02-13T00:00:00.000Z","formattedDate":"February 13, 2022","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":9.735,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.8 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.8-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.9 release notes","permalink":"/blog/2022/05/29/version-2.9-release-notes"},"nextItem":{"title":"Version 2.7 release notes","permalink":"/blog/2021/12/12/version-2.7-release-notes"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/version-2.8-is-here/banner.png)\\n\\nVersion 2.8 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## WebSocket support and `socket.io` integration\\n\\nAs of version 2.8, Foal officially supports WebSockets, allowing you to establish two-way interactive communication between your server(s) and your clients.\\n\\nThe architecture includes: controllers and sub-controllers, hooks, success and error responses, message broadcasting, rooms, use from HTTP controllers, DI, error-handling, validation, unit testing, horizontal scalability, auto-reconnection, etc\\n\\n### Get Started\\n\\n#### Server\\n\\n```bash\\nnpm install @foal/socket.io\\n```\\n\\n*services/websocket.service.ts*\\n```typescript\\nimport { EventName, ValidatePayload, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n @EventName(\'create product\')\\n @ValidatePayload({\\n additionalProperties: false,\\n properties: { name: { type: \'string\' }},\\n required: [ \'name\' ],\\n type: \'object\'\\n })\\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\\n const product = new Product();\\n product.name = payload.name;\\n await product.save();\\n\\n // Send a message to all clients.\\n ctx.socket.broadcast.emit(\'refresh products\');\\n return new WebsocketResponse();\\n }\\n\\n}\\n```\\n\\n*src/index.ts*\\n\\n```typescript\\n// ...\\n\\nasync function main() {\\n const serviceManager = new ServiceManager();\\n\\n const app = await createApp(AppController, { serviceManager });\\n const httpServer = http.createServer(app);\\n\\n // Instanciate, init and connect websocket controllers.\\n await serviceManager.get(WebsocketController).attachHttpServer(httpServer);\\n\\n // ...\\n}\\n\\n```\\n\\n#### Client\\n\\n> This example uses JavaScript code as client, but socket.io supports also [many other languages](https://socket.io/docs/v4) (python, java, etc).\\n\\n```bash\\nnpm install socket.io-client@4\\n```\\n\\n```typescript\\nimport { io } from \'socket.io-client\';\\n\\nconst socket = io(\'ws://localhost:3001\');\\n\\nsocket.on(\'connect\', () => {\\n\\n socket.emit(\'create product\', { name: \'product 1\' }, response => {\\n if (response.status === \'error\') {\\n console.log(response.error);\\n }\\n });\\n\\n});\\n\\nsocket.on(\'connect_error\', () => {\\n console.log(\'Impossible to establish the socket.io connection\');\\n});\\n\\nsocket.on(\'refresh products\', () => {\\n console.log(\'refresh products!\');\\n});\\n```\\n\\n> When using socket.io with FoalTS, the client function `emit` can only take one, two or three arguments.\\n> ```typescript\\n> socket.emit(\'event name\');\\n> socket.emit(\'event name\', { /* payload */ });\\n> // The acknowledgement callback must always be passed in third position.\\n> socket.emit(\'event name\', { /* payload */ }, response => { /* do something */ });\\n> ```\\n\\n### Architecture\\n\\n#### Controllers and hooks\\n\\nThe WebSocket architecture is very similar to the HTTP architecture. They both have controllers and hooks. While HTTP controllers use paths to handle the various application endpoints, websocket controllers use event names. As with HTTP, event names can be extended with subcontrollers.\\n\\n*user.controller.ts*\\n```typescript\\nimport { EventName, WebsocketContext } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n createUser(ctx: WebsocketContext) {\\n // ...\\n }\\n\\n @EventName(\'delete\')\\n deleteUser(ctx: WebsocketContext) {\\n // ...\\n }\\n\\n}\\n```\\n\\n*websocket.controller.ts*\\n```typescript\\nimport { SocketIOController, wsController } from \'@foal/socket.io\';\\n\\nimport { UserController } from \'./user.controller.ts\';\\n\\nexport class WebsocketController extends SocketIOController {\\n subControllers = [\\n wsController(\'users \', UserController)\\n ];\\n}\\n```\\n\\n> Note that the event names are simply concatenated. So you have to manage the spaces between the words yourself if there are any.\\n\\n##### Contexts\\n\\nThe `Context` and `WebsocketContext` classes share common properties such as the `state`, the `user` and the `session`.\\n\\n\\nHowever, unlike their HTTP version, instances of `WebsocketContext` do not have a `request` property but a `socket` property which is the object provided by socket.io. They also have two other attributes: the `eventName` and the `payload` of the request.\\n\\n##### Responses\\n\\nA controller method returns a response which is either a `WebsocketResponse` or a `WebsocketErrorResponse`.\\n\\nIf a `WebsocketResponse(data)` is returned, the server will return to the client an object of this form:\\n```typescript\\n{\\n status: \'ok\',\\n data: data\\n}\\n```\\n\\n\\nIf it is a `WebsocketErrorResponse(error)`, the returned object will look like this:\\n```typescript\\n{\\n status: \'error\',\\n error: error\\n}\\n```\\n\\n> Note that the `data` and `error` parameters are both optional.\\n\\n##### Hooks\\n\\nIn the same way, Foal provides hooks for websockets. They work the same as their HTTP version except that some types are different (`WebsocketContext`, `WebsocketResponse|WebsocketErrorResponse`).\\n\\n```typescript\\nimport { EventName, WebsocketContext, WebsocketErrorResponse, WebsocketHook } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n @WebsocketHook((ctx, services) => {\\n if (typeof ctx.payload.name !== \'string\') {\\n return new WebsocketErrorResponse(\'Invalid name type\');\\n }\\n })\\n createUser(ctx: WebsocketContext) {\\n // ...\\n }\\n}\\n```\\n\\n##### Summary table\\n\\n| HTTP | Websocket |\\n| --- | --- |\\n| `@Get`, `@Post`, etc | `@EventName` |\\n| `controller` | `wsController` |\\n| `Context` | `WebsocketContext` |\\n| `HttpResponse`(s) | `WebsocketResponse`, `WebsocketErrorResponse` |\\n| `Hook` | `WebsocketHook` |\\n| `MergeHooks` | `MergeWebsocketHooks` |\\n| `getHookFunction`, `getHookFunctions` | `getWebsocketHookFunction`, `getWebsocketHookFunctions` |\\n\\n#### Send a message\\n\\nAt any time, the server can send one or more messages to the client using its `socket` object.\\n\\n*Server code*\\n```typescript\\nimport { EventName, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n createUser(ctx: WebsocketContext) {\\n ctx.socket.emit(\'event 1\', \'first message\');\\n ctx.socket.emit(\'event 1\', \'second message\');\\n return new WebsocketResponse();\\n }\\n}\\n```\\n\\n*Client code*\\n```typescript\\nsocket.on(\'event 1\', payload => {\\n console.log(\'Message: \', payload);\\n});\\n```\\n\\n#### Broadcast a message\\n\\nIf a message is to be broadcast to all clients, you can use the `broadcast` property for this.\\n\\n*Server code*\\n```typescript\\nimport { EventName, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n\\n @EventName(\'create\')\\n createUser(ctx: WebsocketContext) {\\n ctx.socket.broadcast.emit(\'event 1\', \'first message\');\\n ctx.socket.broadcast.emit(\'event 1\', \'second message\');\\n return new WebsocketResponse();\\n }\\n}\\n```\\n\\n*Client code*\\n```typescript\\nsocket.on(\'event 1\', payload => {\\n console.log(\'Message: \', payload);\\n});\\n```\\n\\n#### Grouping clients in rooms\\n\\nSocket.io uses the concept of [rooms](https://socket.io/docs/v4/rooms/) to gather clients in groups. This can be useful if you need to send a message to a particular subset of clients.\\n\\n```typescript\\nimport { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n onConnection(ctx: WebsocketContext) {\\n ctx.socket.join(\'some room\');\\n }\\n\\n @EventName(\'event 1\')\\n createUser(ctx: WebsocketContext) {\\n ctx.socket.to(\'some room\').emit(\'event 2\');\\n return new WebsocketResponse();\\n }\\n\\n}\\n```\\n\\n#### Accessing the socket.io server\\n\\nYou can access the socket.io server anywhere in your code (including your HTTP controllers) by injecting the `WsServer` service.\\n\\n```typescript\\nimport { dependency, HttpResponseOK, Post } from \'@foal/core\';\\nimport { WsServer } from \'@foal/socket.io\';\\n\\nexport class UserController {\\n @dependency\\n wsServer: WsServer;\\n\\n @Post(\'/users\')\\n createUser() {\\n // ...\\n this.wsServer.io.emit(\'refresh users\');\\n\\n return new HttpResponseOK();\\n }\\n}\\n```\\n\\n#### Error-handling\\n\\nAny error thrown or rejected in a websocket controller, hook or service, if not caught, is converted to a `WebsocketResponseError`. If the `settings.debug` configuration parameter is `true`, then the error is returned as is to the client. Otherwise, the server returns this response:\\n\\n```typescript\\n({\\n status: \'error\',\\n error: {\\n code: \'INTERNAL_SERVER_ERROR\',\\n message: \'An internal server error has occurred.\'\\n }\\n})\\n```\\n\\n##### Customizing the error handler\\n\\nJust as its HTTP version, the `SocketIOController` class supports an optional `handleError` to override the default error handler.\\n\\n```typescript\\nimport { EventName, renderWebsocketError, SocketIOController, WebsocketContext, WebsocketErrorResponse } from \'@foal/socket.io\';\\n\\nclass PermissionDenied extends Error {}\\n\\nexport class WebsocketController extends SocketIOController implements ISocketIOController {\\n @EventName(\'create user\')\\n createUser() {\\n throw new PermissionDenied();\\n }\\n\\n handleError(error: Error, ctx: WebsocketContext){\\n if (error instanceof PermissionDenied) {\\n return new WebsocketErrorResponse(\'Permission is denied\');\\n }\\n\\n return renderWebsocketError(error, ctx);\\n }\\n}\\n```\\n\\n### Payload Validation\\n\\nFoal provides a default hook `@ValidatePayload` to validate the request payload. It is very similar to its HTTP version `@ValidateBody`.\\n\\n*Server code*\\n```typescript\\nimport { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n @EventName(\'create product\')\\n @ValidatePayload({\\n additionalProperties: false,\\n properties: { name: { type: \'string\' }},\\n required: [ \'name\' ],\\n type: \'object\'\\n })\\n async createProduct(ctx: WebsocketContext, payload: { name: string }) {\\n const product = new Product();\\n product.name = payload.name;\\n await product.save();\\n\\n // Send a message to all clients.\\n ctx.socket.broadcast.emit(\'refresh products\');\\n return new WebsocketResponse();\\n }\\n\\n}\\n```\\n\\n*Validation error response*\\n```typescript\\n({\\n status: \'error\',\\n error: {\\n code: \'VALIDATION_PAYLOAD_ERROR\',\\n payload: [\\n // errors\\n ]\\n }\\n})\\n```\\n\\n### Unit Testing\\n\\nTesting WebSocket controllers and hooks is very similar to testing their HTTP equivalent. The `WebsocketContext` takes three parameters.\\n\\n| Name | Type | Description |\\n| --- | --- | --- |\\n| `eventName` | `string` | The name of the event. |\\n| `payload`| `any` | The request payload. |\\n| `socket` | `any` | The socket (optional). Default: `{}`. |\\n\\n### Advanced\\n\\n#### Multiple node servers\\n\\nThis example shows how to manage multiple node servers using a redis adapter.\\n\\n```bash\\nnpm install @socket.io/redis-adapter@7 redis@3\\n```\\n\\n*websocket.controller.ts*\\n```typescript\\nimport { EventName, SocketIOController, WebsocketContext, WebsocketResponse } from \'@foal/socket.io\';\\nimport { createAdapter } from \'@socket.io/redis-adapter\';\\nimport { createClient } from \'redis\';\\n\\nexport const pubClient = createClient({ url: \'redis://localhost:6379\' });\\nexport const subClient = pubClient.duplicate();\\n\\nexport class WebsocketController extends SocketIOController {\\n adapter = createAdapter(pubClient, subClient);\\n\\n @EventName(\'create user\')\\n createUser(ctx: WebsocketContext) {\\n // Broadcast an event to all clients of all servers.\\n ctx.socket.broadcast.emit(\'refresh users\');\\n return new WebsocketResponse();\\n }\\n}\\n```\\n\\n#### Handling connection\\n\\nIf you want to run some code when a Websocket connection is established (for example to join a room or forward the session), you can use the `onConnection` method of the `SocketIOController` for this.\\n\\n```typescript\\nimport { SocketIOController, WebsocketContext } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n onConnection(ctx: WebsocketContext) {\\n // ...\\n }\\n\\n}\\n```\\n\\n> The context passed in the `onConnection` method has an undefined payload and an empty event name.\\n\\n##### Error-handling\\n\\nAny errors thrown or rejected in the `onConnection` is sent back to the client. So you may need to add a `try {} catch {}` in some cases.\\n\\nThis error can be read on the client using the `connect_error` event listener.\\n\\n```typescript\\nsocket.on(\\"connect_error\\", () => {\\n // Do some stuff\\n socket.connect();\\n});\\n```\\n\\n#### Custom server options\\n\\nCustom options can be passed to the socket.io server as follows. The complete list of options can be found [here](https://socket.io/docs/v4/server-options/).\\n\\n```typescript\\nimport { SocketIOController } from \'@foal/socket.io\';\\n\\nexport class WebsocketController extends SocketIOController {\\n\\n options = {\\n connectTimeout: 60000\\n }\\n\\n}\\n```\\n\\n## Passing a custom database client to a session store\\n\\nBy default, the `MongoDBStore` and `RedisStore` create a new client to connect to their respective databases. The `TypeORMStore` uses the default TypeORM connection.\\n\\nIt is now possible to override this behavior by providing a custom client to the stores at initialization.\\n\\n### `TypeORMStore`\\n\\n*First example*\\n```typescript\\nimport { dependency } from \'@foal/core\';\\nimport { TypeORMStore } from \'@foal/typeorm\';\\nimport { createConnection } from \'typeorm\';\\n\\nexport class AppController {\\n @dependency\\n store: TypeORMStore;\\n\\n // ...\\n\\n async init() {\\n const connection = await createConnection(\'connection2\');\\n this.store.setConnection(connection);\\n }\\n}\\n```\\n\\n*Second example*\\n\\n```typescript\\nimport { createApp, ServiceManager } from \'@foal/core\';\\nimport { TypeORMStore } from \'@foal/typeorm\';\\nimport { createConnection } from \'typeorm\';\\n\\nasync function main() {\\n const connection = await createConnection(\'connection2\');\\n\\n const serviceManager = new ServiceManager();\\n serviceManager.get(TypeORMStore).setConnection(connection);\\n\\n const app = await createApp(AppController, { serviceManager });\\n\\n // ...\\n}\\n```\\n\\n### `RedisStore`\\n\\n```\\nnpm install redis@3\\n```\\n\\n*index.ts*\\n```typescript\\nimport { createApp, ServiceManager } from \'@foal/core\';\\nimport { RedisStore } from \'@foal/redis\';\\nimport { createClient } from \'redis\';\\n\\nasync function main() {\\n const redisClient = createClient(\'redis://localhost:6379\');\\n\\n const serviceManager = new ServiceManager();\\n serviceManager.get(RedisStore).setRedisClient(redisClient);\\n\\n const app = await createApp(AppController, { serviceManager });\\n\\n // ...\\n}\\n```\\n\\n### `MongoDBStore`\\n\\n```\\nnpm install mongodb@3\\n```\\n\\n*index.ts*\\n```typescript\\nimport { createApp, ServiceManager } from \'@foal/core\';\\nimport { MongoDBStore } from \'@foal/mongodb\';\\nimport { MongoClient } from \'mongodb\';\\n\\nasync function main() {\\n const mongoDBClient = await MongoClient.connect(\'mongodb://localhost:27017/db\', {\\n useNewUrlParser: true,\\n useUnifiedTopology: true\\n });\\n\\n const serviceManager = new ServiceManager();\\n serviceManager.get(MongoDBStore).setMongoDBClient(mongoDBClient);\\n\\n const app = await createApp(AppController, { serviceManager });\\n\\n // ...\\n}\\n```\\n\\n## Support for AWS S3 Server-Side Encryption\\n\\nA new configuration option can be provided to the `S3Disk` to support server-side encryption.\\n\\n*Example*\\n\\n\\n\\n```yaml\\nsettings:\\n aws:\\n accessKeyId: xxx\\n secretAccessKey: yyy\\n disk:\\n driver: \'@foal/aws-s3\'\\n s3:\\n bucket: \'uploaded\'\\n serverSideEncryption: \'AES256\'\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"aws\\": {\\n \\"accessKeyId\\": \\"xxx\\",\\n \\"secretAccessKey\\": \\"yyy\\"\\n },\\n \\"disk\\": {\\n \\"driver\\": \\"@foal/aws-s3\\",\\n \\"s3\\": {\\n \\"bucket\\": \\"uploaded\\",\\n \\"serverSideEncryption\\": \\"AES256\\"\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n aws: {\\n accessKeyId: \\"xxx\\",\\n secretAccessKey: \\"yyy\\"\\n },\\n disk: {\\n driver: \\"@foal/aws-s3\\",\\n s3: {\\n bucket: \\"uploaded\\",\\n serverSideEncryption: \\"AES256\\"\\n }\\n }\\n }\\n}\\n```\\n\\n\\n"},{"id":"/2021/12/12/version-2.7-release-notes","metadata":{"permalink":"/blog/2021/12/12/version-2.7-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-12-12-version-2.7-release-notes.md","source":"@site/blog/2021-12-12-version-2.7-release-notes.md","title":"Version 2.7 release notes","description":"Banner","date":"2021-12-12T00:00:00.000Z","formattedDate":"December 12, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.83,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.7 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.7-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.8 release notes","permalink":"/blog/2022/02/13/version-2.8-release-notes"},"nextItem":{"title":"Version 2.6 release notes","permalink":"/blog/2021/09/19/version-2.6-release-notes"}},"content":"![Banner](./assets/version-2.7-is-here/banner.png)\\n\\nVersion 2.7 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## The body of `HttpResponse` can be typed\\n\\nThe `HttpResponse` class becomes generic so as to enforce the type of its `body` property if needed.\\n\\n```typescript\\nimport { Get, HttpResponse } from \'@foal/core\';\\n\\nimport { Product } from \'../entities\';\\n\\nexport class AppController {\\n @Get(\'/products\')\\n async products(): HttpResponse {\\n const products = await Product.find({});\\n return new HttpResponse(products);\\n }\\n}\\n```\\n\\nIt also allows you to infer the type of the body in your tests:\\n\\n![Generic HttpResponse](./assets/version-2.7-is-here/generic-http-response.png)\\n\\n## Support for signed cookies\\n\\nStarting from this version, you can sign cookies and read them through the `signedCookies` attribute.\\n\\n```typescript\\nimport { Context, HttpResponseOK, Get, Post } from \'@foal/core\';\\n\\nclass AppController {\\n @Get(\'/\')\\n index(ctx: Context) {\\n const cookie1: string|undefined = ctx.request.signedCookies.cookie1;\\n // Do something.\\n return new HttpResponseOK();\\n }\\n\\n @Post(\'/sign-cookie\')\\n index() {\\n return new HttpResponseOK()\\n .setCookie(\'cookie1\', \'value1\', {\\n signed: true\\n });\\n }\\n}\\n```\\n\\n> In order to use signed cookies, you must provide a secret with the configuration key `settings.cookieParser.secret`.\\n\\n## Environment name can be provided via `NODE_ENV` or `FOAL_ENV`\\n\\nVersion 2.7 allows to you to specify the environment name (production, development, etc) with the `FOAL_ENV` environment variable.\\n\\nThis can be useful if you have third party libraries whose behavior also depends on the value of `NODE_ENV` (see [Github issue here](https://github.com/FoalTS/foal/issues/1004)).\\n\\n## `foal generate entity` and `foal generate hook` support sub-directories\\n\\n### Example with entities (models)\\n\\n```shell\\nfoal g entity user\\nfoal g entity business/product\\n```\\n\\n*Output*\\n```\\nsrc/\\n \'- app/\\n \'- entities/\\n |- business/\\n | |- product.entity.ts\\n | \'- index.ts\\n |- user.entity.ts\\n \'- index.ts\\n```\\n\\n### Example with hooks\\n\\n```shell\\nfoal g hook log\\nfoal g hook auth/admin-required\\n```\\n\\n*Output*\\n```\\nsrc/\\n \'- app/\\n \'- hooks/\\n |- auth/\\n | |- admin-required.hook.ts\\n | \'- index.ts\\n |- log.hook.ts\\n \'- index.ts\\n```\\n\\n## New `afterPreMiddlewares` option in `createApp`\\n\\nIt is now possible to run a custom middleware after all internal Express middlewares of the framework.\\n\\nThis can be useful in rare situations, for example when using the [RequestContext helper](https://mikro-orm.io/docs/identity-map/#-requestcontext-helper-for-di-containers) in Mikro-ORM.\\n\\n```typescript\\nconst app = await createApp({\\n afterPreMiddlewares: [\\n (req, res, next) => {\\n RequestContext.create(orm.em, next);\\n }\\n ]\\n})\\n```\\n\\n\\n## Contributors\\n\\n- [@MCluck90](https://github.com/MCluck90)\\n- [@kingdun3284](https://github.com/kingdun3284)"},{"id":"/2021/09/19/version-2.6-release-notes","metadata":{"permalink":"/blog/2021/09/19/version-2.6-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-09-19-version-2.6-release-notes.md","source":"@site/blog/2021-09-19-version-2.6-release-notes.md","title":"Version 2.6 release notes","description":"Banner","date":"2021-09-19T00:00:00.000Z","formattedDate":"September 19, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.385,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.6 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.6-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.7 release notes","permalink":"/blog/2021/12/12/version-2.7-release-notes"},"nextItem":{"title":"Version 2.5 release notes","permalink":"/blog/2021/06/11/version-2.5-release-notes"}},"content":"![Banner](./assets/version-2.6-is-here/banner.png)\\n\\nVersion 2.6 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## Support of the `array` value for AJV `coerceTypes` option\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"ajv\\": {\\n \\"coerceTypes\\": \\"array\\"\\n }\\n }\\n}\\n```\\n\\nOption documentation: [https://ajv.js.org/coercion.html#coercion-to-and-from-array](https://ajv.js.org/coercion.html#coercion-to-and-from-array).\\n\\n## Swagger page supports strict CSP\\n\\nInline scripts in the Swagger page have been removed to support more strict *Content Security Policy* directive.\\n\\n## Bug fixes\\n\\nThe `foal connect angular` command now supports empty `angular.json` files."},{"id":"/2021/06/11/version-2.5-release-notes","metadata":{"permalink":"/blog/2021/06/11/version-2.5-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-06-11-version-2.5-release-notes.md","source":"@site/blog/2021-06-11-version-2.5-release-notes.md","title":"Version 2.5 release notes","description":"Banner","date":"2021-06-11T00:00:00.000Z","formattedDate":"June 11, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":0.525,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.5 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.5-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.6 release notes","permalink":"/blog/2021/09/19/version-2.6-release-notes"},"nextItem":{"title":"Version 2.4 release notes","permalink":"/blog/2021/05/19/version-2.4-release-notes"}},"content":"![Banner](./assets/version-2.5-is-here/banner.png)\\n\\nVersion 2.5 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## `npm run develop` watches config files\\n\\nIn previous versions of Foal, the `develop` command did not restart the server when a configuration file was changed. This was annoying and is the reason why, starting with v2.5, new projects generated by the CLI will watch configuration files.\\n\\n## `createOpenApiDocument` accepts an optional serviceManager\\n\\nIf you use `createOpenApiDocument`, in a shell script for example, the function accepts an optional `serviceManager` parameter from this version.\\n\\nThis can be useful if your OpenAPI decorators access controller properties whose values are manually injected."},{"id":"/2021/05/19/version-2.4-release-notes","metadata":{"permalink":"/blog/2021/05/19/version-2.4-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-05-19-version-2.4-release-notes.md","source":"@site/blog/2021-05-19-version-2.4-release-notes.md","title":"Version 2.4 release notes","description":"Banner","date":"2021-05-19T00:00:00.000Z","formattedDate":"May 19, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.01,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.4 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.4-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.5 release notes","permalink":"/blog/2021/06/11/version-2.5-release-notes"},"nextItem":{"title":"Version 2.3 release notes","permalink":"/blog/2021/04/22/version-2.3-release-notes"}},"content":"![Banner](./assets/version-2.4-is-here/banner.png)\\n\\nVersion 2.4 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## `$data` references for validation\\n\\nVersion 2.4 allows you to enable the AJV `$data` option so that you can use the verified data values as validators for other values.\\n\\n*config/default.json*\\n```json\\n{\\n \\"settings\\": {\\n \\"ajv\\": {\\n \\"$data\\": true\\n }\\n }\\n}\\n```\\n\\n*Example of auth controller*\\n```typescript\\nimport { Context, Post, ValidateBody } from \'@foal/core\';\\n\\nexport class AuthController {\\n @Post(\'/signup\')\\n @ValidateBody({\\n type: \'object\',\\n properties: {\\n username: { type: \'string\' },\\n password: { type: \'string\' },\\n // \\"password\\" and \\"confirmPassword\\" should be identical.\\n confirmPassword: {\\n const: {\\n $data: \'1/password\',\\n },\\n type: \'string\',\\n },\\n }\\n required: [ \'username\', \'password\', \'confirmPassword\' ],\\n additionalProperties: false\\n })\\n signup(ctx: Context) {\\n // Do something.\\n }\\n}\\n\\n```\\n\\n## Cache option for file downloading\\n\\nStarting from version 2.4 the `Disk.createHttpResponse` method accepts an optional parameter to specify the value of the `Cache-Control` header.\\n\\n```typescript\\nimport { Context, dependency, Get } from \'@foal/core\';\\nimport { Disk } from \'@foal/storage\';\\n\\nimport { User } from \'../entities\';\\n\\nexport class ProfileController {\\n @dependency\\n disk: Disk;\\n\\n @Get(\'/avatar\')\\n async readProfileImage(ctx: Context) {\\n return this.disk.createHttpResponse(ctx.user.avatar, {\\n cache: \'no-cache\'\\n });\\n }\\n```\\n\\n## Bug fixes\\n\\nSee issue [#930](https://github.com/FoalTS/foal/issues/930).\\n\\n## Contributors\\n\\n[@ZakRabe](https://github.com/ZakRabe)"},{"id":"/2021/04/22/version-2.3-release-notes","metadata":{"permalink":"/blog/2021/04/22/version-2.3-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-04-22-version-2.3-release-notes.md","source":"@site/blog/2021-04-22-version-2.3-release-notes.md","title":"Version 2.3 release notes","description":"Banner","date":"2021-04-22T00:00:00.000Z","formattedDate":"April 22, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":2.07,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.3 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.3-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.4 release notes","permalink":"/blog/2021/05/19/version-2.4-release-notes"},"nextItem":{"title":"What\'s new in version 2 (part 4/4)","permalink":"/blog/2021/04/08/whats-new-in-version-2-part-4"}},"content":"![Banner](./assets/version-2.3-is-here/banner.png)\\n\\nVersion 2.3 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## GraphiQL\\n\\nFrom version 2.3, it is possible to generate a GraphiQL page in one line of code. This can be useful if you quickly need to test your API.\\n\\n```bash\\nnpm install @foal/graphiql\\n```\\n\\n![GraphiQL](./assets/version-2.3-is-here/graphiql.png)\\n\\n*app.controller.ts*\\n```typescript\\nimport { GraphiQLController } from \'@foal/graphiql\';\\n\\nimport { GraphqlApiController } from \'./services\';\\n\\nexport class AppController {\\n\\n subControllers = [\\n // ...\\n controller(\'/graphql\', GraphqlApiController),\\n controller(\'/graphiql\', GraphiQLController)\\n ];\\n\\n}\\n```\\n\\nThe page is also customizable and you can provide additional options to change the UI or the API endpoint.\\n\\n```typescript\\nexport class GraphiQL2Controller extends GraphiQLController {\\n\\n cssThemeURL = \'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.23.0/theme/solarized.css\';\\n\\n apiEndpoint = \'/api\';\\n\\n options: GraphiQLControllerOptions = {\\n docExplorerOpen: true,\\n editorTheme: \'solarized light\'\\n }\\n\\n}\\n\\n```\\n\\n## Support for `.env.local` files\\n\\nFoal\'s configuration system already supported `.env` files in previous versions. As of version 2.3, the framework also supports `.env.local` files.\\n\\nThis can be useful in case you want to have two `.env` files, one to define the default env vars needed by the application and another to override these values on your local machine.\\n\\nIf a variable is defined in both files, the value in the `.env.local` file will take precedence.\\n\\nSimilarly, you can also define environment-specific local files (`.env.development.local`, `.env.production.local`, etc).\\n\\n## Prisma documentation\\n\\nThe documentation has been expanded to include [examples](https://foalts.org/docs/databases/other-orm/introduction) of how to use Prisma with Foal.\\n\\n## Base 64 and base 64 URL utilities\\n\\nTwo functions are provided to convert base64 encoded strings to base64url encoded strings and vice versa.\\n\\n```typescript\\nimport { convertBase64ToBase64url, convertBase64urlToBase64 } from \'@foal/core\';\\n\\nconst str = convertBase64ToBase64url(base64Str);\\nconst str2 = convertBase64urlToBase64(base64urlStr);\\n```\\n\\n## Converting Streams to Buffers\\n\\nIn case you need to convert a readable stream to a concatenated buffer during testing, you can now use the `streamToBuffer` function for this.\\n\\n```typescript\\nimport { streamToBuffer } from \'@foal/core\';\\n\\nconst buffer = await streamToBuffer(stream);\\n```\\n\\n## Accessing services during authentication\\n\\nThe `user` option of `@JWTRequired` and `@UseSessions` now gives you the possibility to access services.\\n\\n```typescript\\nclass UserService {\\n getUser(id) {\\n return User.findOne({ id });\\n }\\n}\\n\\n@JWTRequired({\\n user: (id, services) => services.get(UserService).getUser(id)\\n})\\nclass ApiController {\\n @Get(\'/products\')\\n getProducts(ctx: Context) {\\n // ctx.user is the object returned by UserService.\\n }\\n}\\n\\n```\\n\\n## Bug Fixes\\n\\n### Social authentication\\n\\nSocial authentication controllers could sometimes return 500 errors, depending on the social provider you were using. This was due to a problem of string encoding in the callback URL. This bug has been fixed in this version."},{"id":"/2021/04/08/whats-new-in-version-2-part-4","metadata":{"permalink":"/blog/2021/04/08/whats-new-in-version-2-part-4","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-04-08-whats-new-in-version-2-part-4.md","source":"@site/blog/2021-04-08-whats-new-in-version-2-part-4.md","title":"What\'s new in version 2 (part 4/4)","description":"Banner","date":"2021-04-08T00:00:00.000Z","formattedDate":"April 8, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":5.675,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 4/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-4.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.3 release notes","permalink":"/blog/2021/04/22/version-2.3-release-notes"},"nextItem":{"title":"What\'s new in version 2 (part 3/4)","permalink":"/blog/2021/03/11/whats-new-in-version-2-part-3"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-4/banner.png)\\n\\nThis article presents the improvements to the session system in FoalTS version 2.\\n\\nThe new syntax can be used either with cookies or with the `Authorization` header. It adds the following new features:\\n- query all sessions of a given user\\n- query all connected users\\n- force logout of a specific user\\n- flash sessions\\n- session ID regeneration\\n- anonymous and authenticated sessions\\n\\nFoalTS also simplifies stateful CSRF protection so that all it takes is one setting to enable it.\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 4 of the series of articles *What\'s new in version 2.0*. Part 3 can be found [here](./2021-03-11-whats-new-in-version-2-part-3.md).\\n\\n## New Session System\\n\\nThe new authentication system is probably the main new feature of version 2. The old session components have been redesigned so as to serve three purposes:\\n- be easy to use with very little code,\\n- support a large variety of applications and architectures (SPA, Mobile, SSR, API, `Authorization` header, cookies, serverless environment, social auth, etc),\\n- and add missing features impossible to implement in version 1.\\n\\nHere is the way to use it:\\n- First [specify in the configuration](/docs/authentication/session-tokens#choosing-a-session-store) where your sessions should be stored (SQL database, redis, Mongo, etc).\\n- Then decorate any route or controller that need authentication with `@UseSessions`.\\n\\n### Example with the `Authorization` header\\n\\nIn this first example, we\'d like to use the `Authorization` header to handle authentication.\\n\\nWe want to send an email address and password to `/login` and retrieve a token in return to authenticate further requests.\\n\\n```typescript\\nimport { dependency, Context, Get, HttpResponseOK, UserRequired, UseSessions, ValidateBody, HttpResponseUnauthorized, Post } from \'@foal/core\';\\nimport { fetchUser } from \'@foal/typeorm\';\\n\\nimport { User, Product } from \'../entities\';\\n\\n@UseSessions({\\n user: fetchUser(User)\\n})\\nexport class ApiController {\\n @dependency\\n store: Store;\\n\\n @Get(\'/products\')\\n @UserRequired()\\n async readProducts(ctx: Context) {\\n return new HttpResponseOK(Product.find({ user: ctx.user }));\\n }\\n\\n @Post(\'/login\')\\n @ValidateBody({\\n additionalProperties: false,\\n properties: {\\n email: { type: \'string\', format: \'email\' },\\n password: { type: \'string\' }\\n },\\n required: [ \'email\', \'password\' ],\\n type: \'object\',\\n })\\n async login(ctx: Context) {\\n const user = await User.findOne({ email: ctx.request.body.email });\\n\\n if (!user) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n ctx.session = await createSession(this.store);\\n ctx.session.setUser(user);\\n\\n return new HttpResponseOK({\\n token: ctx.session.getToken()\\n });\\n }\\n\\n @Post(\'/logout\')\\n async logout(ctx: Context) {\\n if (ctx.session) {\\n await ctx.session.destroy();\\n }\\n\\n return new HttpResponseOK();\\n }\\n}\\n```\\n\\n### Example with cookies\\n\\nIn this second example, we will use cookies to manage authentication. Foal will auto-creates a session when none exists.\\n\\n```typescript\\nimport { dependency, Context, Get, HttpResponseOK, UserRequired, UseSessions, ValidateBody, HttpResponseUnauthorized, Post } from \'@foal/core\';\\nimport { fetchUser } from \'@foal/typeorm\';\\n\\nimport { User, Product } from \'../entities\';\\n\\n@UseSessions({\\n // highlight-next-line\\n cookie: true,\\n user: fetchUser(User)\\n})\\nexport class ApiController {\\n @dependency\\n store: Store;\\n\\n @Get(\'/products\')\\n @UserRequired()\\n async readProducts(ctx: Context) {\\n return new HttpResponseOK(Product.find({ user: ctx.user }));\\n }\\n\\n @Post(\'/login\')\\n @ValidateBody({\\n additionalProperties: false,\\n properties: {\\n email: { type: \'string\', format: \'email\' },\\n password: { type: \'string\' }\\n },\\n required: [ \'email\', \'password\' ],\\n type: \'object\',\\n })\\n async login(ctx: Context) {\\n const user = await User.findOne({ email: ctx.request.body.email });\\n\\n if (!user) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\\n return new HttpResponseUnauthorized();\\n }\\n\\n // highlight-next-line\\n ctx.session.setUser(user);\\n\\n // highlight-next-line\\n return new HttpResponseOK();\\n }\\n\\n @Post(\'/logout\')\\n async logout(ctx: Context) {\\n if (ctx.session) {\\n await ctx.session.destroy();\\n }\\n\\n return new HttpResponseOK();\\n }\\n}\\n```\\n\\n### New features\\n\\nIn addition to this redesign, version 2 also offers new features.\\n\\n#### Query all sessions of a user (TypeORM only)\\n\\nThis feature allows you to list all sessions associated with a specific user. This can be useful if a user is connected on several devices and you like to audit them.\\n\\n```typescript\\nconst user = { id: 1 };\\nconst ids = await store.getSessionIDsOf(user);\\n```\\n\\n#### Query all connected users (TypeORM only)\\n\\nThis feature lists all users that have at least one session in the database.\\n\\n```typescript\\nconst ids = await store.getAuthenticatedUserIds();\\n```\\n\\n#### Force the disconnection of a user (TypeORM only)\\n\\nIn case you want to remove all sessions associated with a specific user, you can use the `destroyAllSessionsOf` method. This can be useful if you think a session has been corrupted or when you want, for example when a password is changed, to disconnect a user from all other devices to which he/she has previously logged on.\\n\\n```typescript\\nconst user = { id: 1 };\\nawait store.destroyAllSessionsOf(user);\\n```\\n\\n#### Flash sessions\\n\\nFlash content is used when we want to save data (a message for example) only for the next request. A typical use case is when a user enters wrong credentials. The page is refreshed and an error message is displayed.\\n\\nTo use flash content, you only need to add the option `flash` set to `true` in the `set` method.\\n\\n```typescript\\nctx.session.set(\'error\', \'Incorrect email or password\', { flash: true });\\n```\\n\\n#### Regenerate the session ID\\n\\nRegenerating the session ID is a [recommended practice](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change) when changing a user\'s privileges or password. This can now be done with the `regenerateID` method\\n\\n```typescript\\nawait ctx.session.regenerateID();\\n```\\n\\n#### Expired sessions clean up regularly (TypeORM and MongoDB)\\n\\nStarting from version 2, Foal regularly cleanup expired sessions in your database so you don\'t have to do it manually.\\n\\n#### Anonymous sessions and templates\\n\\nIn version 2, `@UseSessions({ cookie: true })` automatically creates a session if none exists. This is particularly useful if you\'re building a shopping website with SSR templates. When the user navigates on the website, he/she can add items to the cart without having to log in the first place. Then, when the user wants to place his/her order, he can log in and the only thing you have to do is this:\\n\\n```typescript\\nctx.session.setUser(user)\\n```\\n\\n## Stateful CSRF protection simplified\\n\\nIn version 1, providing a CSRF protection was quite complex. We needed to manage token generation, handle the CSRF cookie (expiration, etc), use an additional hook, etc.\\n\\nStarting from version 2, the CSRF protection is all managed by `@UseSessions`.\\n\\n\\n\\n\\n\\n```yaml\\nsettings:\\n session:\\n csrf:\\n enabled: true\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"session\\": {\\n \\"csrf\\": {\\n \\"enabled\\": true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n session: {\\n csrf: {\\n enabled: true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\nWhen it is enabled, an additional `XSRF-TOKEN` cookie is sent to the client at the same time as the session cookie. It contains a CSRF token associated with your session.\\n\\nWhen a request is made to the server, the `@UseSessions` hooks expects you to include its value in the `XSRF-TOKEN` header.\\n\\n> If you\'re building a regular web application and want to include the CSRF token in your templates, you can retrieve it with this statement: `ctx.session.get(\'csrfToken\')`."},{"id":"/2021/03/11/whats-new-in-version-2-part-3","metadata":{"permalink":"/blog/2021/03/11/whats-new-in-version-2-part-3","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-03-11-whats-new-in-version-2-part-3.md","source":"@site/blog/2021-03-11-whats-new-in-version-2-part-3.md","title":"What\'s new in version 2 (part 3/4)","description":"Banner","date":"2021-03-11T00:00:00.000Z","formattedDate":"March 11, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":3.665,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 3/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-3.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 4/4)","permalink":"/blog/2021/04/08/whats-new-in-version-2-part-4"},"nextItem":{"title":"What\'s new in version 2 (part 2/4)","permalink":"/blog/2021/03/02/whats-new-in-version-2-part-2"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-3/banner.png)\\n\\nThis article presents some improvements introduced in version 2 of FoalTS:\\n- the JWT utilities to manage secrets and RSA keys,\\n- the JWT utilities to manage cookies,\\n- and the new stateless CSRF protection.\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 3 of the series of articles *What\'s new in version 2.0*. Part 2 can be found [here](./2021-03-02-whats-new-in-version-2-part-2.md).\\n\\n## New JWT utilities\\n\\n### Accessing config secrets and public/private keys\\n\\nStarting from version 2, there is a standardized way to provide and retrieve JWT secrets and RSA public/private keys: the functions `getSecretOrPublicKey` and `getSecretOrPrivateKey`.\\n\\n#### Using secrets\\n\\nIn this example, a base64-encoded secret is provided in the configuration.\\n\\n*.env*\\n```\\nJWT_SECRET=\\"Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8=\\"\\n```\\n\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n secret: \\"env(JWT_SECRET)\\"\\n secretEncoding: base64\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"secret\\": \\"env(JWT_SECRET)\\",\\n \\"secretEncoding\\": \\"base64\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n jwt: {\\n secret: \\"env(JWT_SECRET)\\",\\n secretEncoding: \\"base64\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\nBoth `getSecretOrPublicKey` and `getSecretOrPrivateKey` functions will return the secret.\\n\\nIn the case a `secretEncoding` value is provided, the functions return a buffer which is the secret decoded with the provided encoding.\\n\\n#### Using public and private keys\\n\\n```javascript\\nconst { Env } = require(\'@foal/core\');\\nconst { readFileSync } = require(\'fs\');\\n\\nmodule.exports = {\\n settings: {\\n jwt: {\\n privateKey: Env.get(\'RSA_PRIVATE_KEY\') || readFileSync(\'./id_rsa\', \'utf8\'),\\n publicKey: Env.get(\'RSA_PUBLIC_KEY\') || readFileSync(\'./id_rsa.pub\', \'utf8\'),\\n }\\n }\\n}\\n```\\n\\nIn this case, `getSecretOrPublicKey` and `getSecretOrPrivateKey` return the keys from the environment variables `RSA_PUBLIC_KEY` and `RSA_PRIVATE_KEY` if they are defined or from the files `id_rsa` and `id_rsa.pub` otherwise.\\n\\n### Managing cookies\\n\\nIn version 2, Foal provides two dedicated functions to manage JWT with cookies. Using these functions instead of manually setting the cookie has three benefits:\\n- they include a CSRF protection (see section below),\\n- the function `setAuthCookie` automatically sets the cookie expiration based on the token expiration,\\n- and cookie options can be provided through the configuration.\\n\\n**Example**\\n\\n*api.controller.ts*\\n```typescript\\nimport { JWTRequired } from \'@foal/jwt\';\\n\\n@JWTRequired({ cookie: true })\\nexport class ApiController {\\n // ...\\n}\\n```\\n\\n*auth.controller.ts*\\n```typescript\\nexport class AuthController {\\n\\n @Post(\'/login\')\\n async login(ctx: Context) {\\n // ...\\n\\n const response = new HttpResponseNoContent();\\n // Do not forget the \\"await\\" keyword.\\n await setAuthCookie(response, token);\\n return response;\\n }\\n\\n @Post(\'/logout\')\\n logout(ctx: Context) {\\n // ...\\n\\n const response = new HttpResponseNoContent();\\n removeAuthCookie(response);\\n return response;\\n }\\n\\n}\\n```\\n\\n**Cookie options**\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n cookie:\\n name: mycookiename # Default: auth\\n domain: example.com\\n httpOnly: true # Warning: unlike session tokens, the httpOnly directive has no default value.\\n path: /foo # Default: /\\n sameSite: strict # Default: lax if settings.jwt.csrf.enabled is true.\\n secure: true\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"cookie\\": {\\n \\"name\\": \\"mycookiename\\",\\n \\"domain\\": \\"example.com\\",\\n \\"httpOnly\\": true,\\n \\"path\\": \\"/foo\\",\\n \\"sameSite\\": \\"strict\\",\\n \\"secure\\": true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n jwt: {\\n cookie: {\\n name: \\"mycookiename\\",\\n domain: \\"example.com\\",\\n httpOnly: true,\\n path: \\"/foo\\",\\n sameSite: \\"strict\\",\\n secure: true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n## Stateless CSRF protection simplified\\n\\nIn version 1, providing a CSRF protection was quite complex. We needed to provide another secret, generate a stateless token, manage the CSRF cookie (expiration, etc), use an additional hook, etc.\\n\\nStarting from version 2, the CSRF protection is all managed by `@JWTRequired`, `setAuthCookie` and `removeAuthCookie`.\\n\\nThe only thing that you have to do it to enable it through the configuration:\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n csrf:\\n enabled: true\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"csrf\\": {\\n \\"enabled\\": true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n jwt: {\\n csrf: {\\n enabled: true\\n }\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\nWhen it is enabled, an additional `XSRF-TOKEN` cookie is sent to the client at the same time as the auth cookie (containing your JWT). It contains a stateless CSRF token which is signed and has the same expiration date as your JWT.\\n\\nWhen a request is made to the server, the `@JWTRequired` hooks expects you to include its value in the `XSRF-TOKEN` header."},{"id":"/2021/03/02/whats-new-in-version-2-part-2","metadata":{"permalink":"/blog/2021/03/02/whats-new-in-version-2-part-2","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-03-02-whats-new-in-version-2-part-2.md","source":"@site/blog/2021-03-02-whats-new-in-version-2-part-2.md","title":"What\'s new in version 2 (part 2/4)","description":"Banner","date":"2021-03-02T00:00:00.000Z","formattedDate":"March 2, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":5.055,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 2/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-2.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 3/4)","permalink":"/blog/2021/03/11/whats-new-in-version-2-part-3"},"nextItem":{"title":"Version 2.2 release notes","permalink":"/blog/2021/02/25/version-2.2-release-notes"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-2/banner.png)\\n\\nThis article presents some improvements introduced in version 2 of FoalTS:\\n- Configuration and type safety\\n- Configuration and `.env` files (`.env`, `.env.test`, etc)\\n- Available configuration file formats (JSON, YAML and JS)\\n- OpenAPI schemas and validation\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 2 of the series of articles *What\'s new in version 2.0*. Part 1 can be found [here](./2021-02-17-whats-new-in-version-2-part-1.md).\\n\\n## New Config System\\n\\n### Type safety\\n\\nStarting from version 2, a great attention is paid to type safety in the configuration. The `Config.get` method allows you specify which type you expect.\\n\\n```typescript\\nconst timeout = Config.get(\'custom.timeout\', \'number\');\\n// The TypeScript type returned by `get` is number|undefined.\\n```\\n\\nIn this example, when calling the `get` method, the framework will look at the configuration files to retrieve the desired value.\\n- If the value is not defined, the function returns `undefined`.\\n- If the value is a number, the function returns it.\\n- If the value is a string that can be converted to a number (ex: `\\"1\\"`), the function converts and returns it.\\n- If the value is not a number and cannot be converted, then the function throws a `ConfigTypeError` with the details. Note that the config value is not logged to avoid leaking sensitive information.\\n\\nIf you wish to make the config parameter mandatory, you can do it by using the `getOrThrow` method. If no value is found, then a `ConfigNotFound` error is thrown.\\n\\n```typescript\\nconst timeout = Config.getOrThrow(\'custom.timeout\', \'number\');\\n// The TypeScript type returned by `get` is number.\\n```\\n\\nSupported types are `string`, `number`, `boolean`, `boolean|string`, `number|string` and `any`.\\n\\n### Multiple `.env` files support\\n\\nVersion 2 allows you to use different `.env` files depending on your environment.\\n\\nIf you configuration is as follows and `NODE_ENV` equals `production`, then the framework will look at `.env.production` to retrieve the value and if it does not exist (the file or the value), Foal will look at `.env`.\\n\\n\\n\\n\\n```yaml\\nsettings:\\n jwt:\\n secret: env(SETTINGS_JWT_SECRET)\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"jwt\\": {\\n \\"secret\\": \\"env(SETTINGS_JWT_SECRET)\\",\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nconst { Env } = require(\'@foal/core\');\\n\\nmodule.exports = {\\n settings: {\\n jwt: {\\n secret: Env.get(\'SETTINGS_JWT_SECRET\')\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n\\n### Three config formats (JS, JSON, YAML)\\n\\nJSON and YAML were already supported in version 1. Starting from version 2, JS is also allowed.\\n\\n\\n\\n\\n```yaml\\nsettings:\\n session:\\n store: \\"@foal/typeorm\\"\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"session\\": {\\n \\"store\\": \\"@foal/typeorm\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nmodule.exports = {\\n settings: {\\n session: {\\n store: \\"@foal/typeorm\\"\\n }\\n }\\n}\\n```\\n\\n\\n\\n\\n### More Liberty in Naming Environment Variables\\n\\nIn version 1, the names of the environment variables were depending on the names of the configuration keys. For example, when using `Config.get(\'settings.mongodbUri\')`, Foal was looking at `SETTINGS_MONGODB_URI`.\\n\\nStarting from version 2, it is your responsability to choose the environement variable that you want to use (if you use one). This gives more flexibility especially when a Cloud provider defines its own variable names.\\n\\n\\n\\n\\n```yaml\\nsettings:\\n mongodbUri: env(MONGODB_URI)\\n```\\n\\n\\n\\n\\n```json\\n{\\n \\"settings\\": {\\n \\"mongodbUri\\": \\"env(MONGODB_URI)\\"\\n }\\n}\\n```\\n\\n\\n\\n\\n```javascript\\nconst { Env } = require(\'@foal/core\');\\n\\nmodule.exports = {\\n settings: {\\n mongodbUri: Env.get(\'MONGODB_URI\')\\n }\\n}\\n```\\n\\n\\n\\n\\n## OpenAPI Schemas & Validation\\n\\nStarting from version 1, Foal has allowed you to generate a complete [Swagger](https://swagger.io/tools/swagger-ui/) interface by reading your code. If your application has validation and auth hooks for example, Foal will use them to generate the proper interface.\\n\\nThis is a handy if you want to quickly test and document your API. Then you can customize it in your own way if you wish and complete and override the OpenAPI spec generated by the framework.\\n\\nIn version 2, support of Swagger has been increased to allow you to define OpenAPI schemas and re-use them for validation.\\n\\nHere is an example.\\n\\n*product.controller.ts*\\n```typescript\\nimport { ApiDefineSchema, ApiResponse, Context, Get, HttpResponseNotFound, HttpResponseOK, Post, ValidateBody, ValidatePathParam } from \'@foal/core\';\\nimport { Product } from \'../../entities\';\\n\\n// First we define the OpenAPI schema \\"Product\\".\\n@ApiDefineSchema(\'Product\', {\\n type: \'object\',\\n properties: {\\n id: { type: \'number\' },\\n name: { type: \'string\' }\\n },\\n additionalProperties: false,\\n required: [\'id\', \'name\'],\\n})\\nexport class ProductController {\\n\\n @Post(\'/\')\\n // We use the schema \\"Product\\" here to validate the request body.\\n @ValidateBody({ $ref: \'#/components/schemas/Product\' })\\n async createProduct(ctx: Context) {\\n const result = await Product.insert(ctx.request.body);\\n return new HttpResponseOK(result.identifiers[0]);\\n }\\n\\n @Get(\'/:productId\')\\n // We use the schema \\"Product\\" here to validate the URL parameter.\\n @ValidatePathParam(\'productId\', { $ref: \'#/components/schemas/Product/properties/id\' })\\n // We give some extra information on the format of the response.\\n @ApiResponse(200, {\\n description: \'Product found in the database\',\\n content: {\\n \'application/json\': { schema: { $ref: \'#/components/schemas/Product\' } }\\n }\\n })\\n async readProduct(ctx: Context, { productId }) {\\n const product = await Product.findOne({ id: productId });\\n\\n if (!product) {\\n return new HttpResponseNotFound();\\n }\\n\\n return new HttpResponseOK(product);\\n }\\n\\n}\\n\\n```\\n\\n*api.controller.ts*\\n```typescript\\nimport { ApiInfo, ApiServer, Context, controller, Get, HttpResponseOK } from \'@foal/core\';\\nimport { ProductController } from \'./api\';\\n\\n// We provide the \\"info\\" metadata to describe the API.\\n@ApiInfo({\\n title: \'My API\',\\n version: \'0.1.0\'\\n})\\n@ApiServer({\\n url: \'/api\'\\n})\\nexport class ApiController {\\n subControllers = [\\n controller(\'/products\', ProductController)\\n ];\\n \\n}\\n```\\n\\n*openapi.controller.ts*\\n```typescript\\nimport { SwaggerController } from \'@foal/swagger\';\\nimport { ApiController } from \'./api.controller\';\\n\\n// This controller generates the Swagger interface.\\nexport class OpenapiController extends SwaggerController {\\n\\n options = {\\n controllerClass: ApiController,\\n }\\n\\n}\\n\\n```\\n\\n*app.controller.ts*\\n```typescript\\nimport { controller, IAppController } from \'@foal/core\';\\nimport { createConnection } from \'typeorm\';\\n\\nimport { ApiController, OpenapiController } from \'./controllers\';\\n\\nexport class AppController implements IAppController {\\n subControllers = [\\n controller(\'/api\', ApiController),\\n controller(\'/swagger\', OpenapiController),\\n ];\\n\\n async init() {\\n await createConnection();\\n }\\n}\\n\\n```\\n\\n![Swagger 1](./assets/whats-new-in-version-2-part-2/swagger.png)\\n\\n![Swagger 2](./assets/whats-new-in-version-2-part-2/swagger2.png)\\n\\n![Swagger 3](./assets/whats-new-in-version-2-part-2/swagger3.png)"},{"id":"/2021/02/25/version-2.2-release-notes","metadata":{"permalink":"/blog/2021/02/25/version-2.2-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-02-25-version-2.2-release-notes.md","source":"@site/blog/2021-02-25-version-2.2-release-notes.md","title":"Version 2.2 release notes","description":"Banner","date":"2021-02-25T00:00:00.000Z","formattedDate":"February 25, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.955,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.2 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/version-2.2-release-notes.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 2/4)","permalink":"/blog/2021/03/02/whats-new-in-version-2-part-2"},"nextItem":{"title":"What\'s new in version 2 (part 1/4)","permalink":"/blog/2021/02/17/whats-new-in-version-2-part-1"}},"content":"![Banner](./assets/version-2.2-is-here/banner.png)\\n\\nVersion 2.2 of Foal has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## New Look of the `createapp` Command\\n\\nThe output of the `createapp` command has been prettified to be more \\"welcoming\\".\\n\\n![New createapp look](./assets/version-2.2-is-here/new-create-app.png)\\n\\n## Authentication Improvement for Single-Page Applications (SPA)\\n\\nWhen building a SPA with cookie-based authentication, it can sometimes be difficult to know if the user is logged in or to obtain certain information about the user (`isAdmin`, etc).\\n\\nSince the authentication token is stored in a cookie with the `httpOnly` directive set to `true` (to mitigate XSS attacks), the front-end application has no way of knowing if a user is logged in, except by making an additional request to the server.\\n\\nTo solve this problem, version 2.2 adds a new option called `userCookie` that allows you to set an additional cookie that the frontend can read with the content you choose. This cookie is synchronized with the session and is refreshed at each request and destroyed when the session expires or when the user logs out.\\n\\nIn the following example, the `user` cookie is empty if no user is logged in or contains certain information about him/her otherwise. This is particularly useful if you need to display UI elements based on user characteristics.\\n\\n*Server-side code*\\n\\n```typescript\\nfunction userToJSON(user: User|undefined) {\\n if (!user) {\\n return \'null\';\\n }\\n\\n return JSON.stringify({\\n email: user.email,\\n isAdmin: user.isAdmin\\n });\\n}\\n\\n@UseSessions({\\n cookie: true,\\n user: fetchUser(User),\\n userCookie: (ctx, services) => userToJSON(ctx.user)\\n})\\nexport class ApiController {\\n\\n @Get(\'/products\')\\n @UserRequired()\\n async readProducts(ctx: Context) {\\n const products = await Product.find({ owner: ctx.user });\\n return new HttpResponseOK(products);\\n }\\n\\n}\\n```\\n\\n*Cookies*\\n\\n![User cookie](./assets/version-2.2-is-here/user-cookie.png)\\n\\n*Client-side code*\\n\\n```javascript\\nconst user = JSON.parse(decodeURIComponent(/* cookie value */));\\n```\\n\\n## Support of Nested Routes in `foal generate|g rest-api `\\n\\nLike the command `g controller`, `g rest-api` now supports nested routes.\\n\\nLet\'s say we have the following file structure:\\n\\n```\\nsrc/\\n \'- app/\\n |- controllers/\\n | |- api.controller.ts\\n | \'- index.ts\\n \'- entities/\\n |- user.entity.ts\\n \'- index.ts\\n```\\n\\nRunning these commands will add and register the following files:\\n\\n```\\nfoal generate rest-api api/product --auth --register\\nfoal generate rest-api api/order --auth --register\\n```\\n\\n```\\nsrc/\\n \'- app/\\n |- controllers/\\n | |- api/\\n | | |- product.controller.ts\\n | | |- order.controller.ts\\n | | \'- index.ts\\n | |- api.controller.ts\\n | \'- index.ts\\n \'- entities/\\n |- product.entity.ts\\n |- order.entity.ts\\n |- user.entity.ts\\n \'- index.ts\\n```"},{"id":"/2021/02/17/whats-new-in-version-2-part-1","metadata":{"permalink":"/blog/2021/02/17/whats-new-in-version-2-part-1","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-02-17-whats-new-in-version-2-part-1.md","source":"@site/blog/2021-02-17-whats-new-in-version-2-part-1.md","title":"What\'s new in version 2 (part 1/4)","description":"Banner","date":"2021-02-17T00:00:00.000Z","formattedDate":"February 17, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":4.69,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"What\'s new in version 2 (part 1/4)","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","image":"blog/twitter-banners/whats-new-in-version-2-part-1.png","tags":["release"]},"unlisted":false,"prevItem":{"title":"Version 2.2 release notes","permalink":"/blog/2021/02/25/version-2.2-release-notes"},"nextItem":{"title":"Version 2.1 release notes","permalink":"/blog/2021/02/03/version-2.1-release-notes"}},"content":"import Tabs from \'@theme/Tabs\';\\nimport TabItem from \'@theme/TabItem\';\\n\\n![Banner](./assets/whats-new-in-version-2-part-1/banner.png)\\n\\nThis article presents some improvements introduced in version 2 of FoalTS:\\n- the new CLI commands\\n- the service and application initialization\\n- the `AppController` interface\\n- custom error-handling & hook post functions\\n- accessing file metadata during uploads\\n\\n\x3c!--truncate--\x3e\\n\\n> This article is the part 1 of the series of articles *What\'s new in version 2.0*. Part 2 can be found [here](./2021-03-02-whats-new-in-version-2-part-2.md).\\n\\n## New CLI commands\\n\\nIn version 1, there were many commands to use, and this, in a specific order. Running and generating migrations from model changes required four commands and building the whole application needed three.\\n\\nIn version 2, the number of CLI commands has been reduced and they have been simplified so that one action matches one command.\\n\\n### Generating migrations\\n\\nThis command generates migrations by comparing the current database schema and the latest changes in your models.\\n\\n\\n\\n\\n```bash\\nnpm run makemigrations\\n```\\n\\n\\n\\n\\n```bash\\nnpm run build:app\\nnpm run migration:generate -- -n my_migration\\n```\\n\\n\\n\\n\\n\\n### Running migrations\\n\\nThis command builds and runs all migrations.\\n\\n\\n\\n\\n```bash\\nnpm run migrations\\n```\\n\\n\\n\\n\\n```bash\\nnpm run build:migrations\\nnpm run migration:run\\n```\\n\\n\\n\\n\\n### Build and run scripts in watch mode (development)\\n\\nIf you want to re-build your scripts each time a file is change, you can execute `npm run develop` in a separate terminal.\\n\\n\\n\\n\\n```bash\\n# In one terminal:\\nnpm run develop\\n\\n# In another terminal:\\nfoal run my-script\\n```\\n\\n\\n\\n\\n```bash\\n# In one terminal:\\nnpm run build:scripts:w\\n\\n# In another terminal:\\nfoal run my-script\\n```\\n\\n\\n\\n\\n### Revert one migration\\n\\nThis command reverts the last executed migration.\\n\\n\\n\\n\\n```bash\\nnpm run revertmigration\\n```\\n\\n\\n\\n\\n```bash\\nnpm run migration:revert\\n```\\n\\n\\n\\n\\n### Build migrations, scripts and the app\\n\\nThis command builds the application, the scripts and the migrations. Unit and e2e tests are not included.\\n\\n\\n\\n\\n```bash\\nnpm run build\\n```\\n\\n\\n\\n\\n```bash\\nnpm run build:app\\nnpm run build:scripts\\nnpm run build:migrations\\n```\\n\\n\\n\\n\\n## Service and Application Initialization\\n\\nIn version 1, it was possible to add an `init` method to the `AppController` class and `boot` methods in the services to initialize the application. These features needed special options in order to be activated.\\n\\nStarting from version 2, they are enabled by default.\\n\\n```typescript\\nexport class AppController {\\n // ...\\n\\n init() {\\n // Execute some code.\\n }\\n}\\n```\\n\\n```typescript\\nexport class MyService {\\n // ...\\n\\n boot() {\\n // Execute some code.\\n }\\n}\\n```\\n\\n## The `AppController` interface\\n\\nThis optional interface allows you to check that the `subControllers` property has the correct type as well as the `init` and `handleError` methods.\\n\\n```typescript\\nexport class AppController implements IAppController {\\n subControllers = [\\n controller(\'/api\', ApiController)\\n ];\\n\\n init() {\\n // ...\\n }\\n\\n handleError(error, ctx) {\\n // ...\\n }\\n}\\n```\\n\\n## Custom Error-Handling & Hook Post Functions\\n\\nIn version 1, when an error was thrown or rejected in a hook or a controller method, the remaining hook post functions were not executed.\\n\\nStarting from version 2, the error is directly converted to an `HttpResponseInternalServerError` and passed to the next post hook functions.\\n\\nThis can be useful in case we want to use exceptions as HTTP responses without breaking the hook post functions.\\n\\n*Example*\\n```typescript\\nclass PermissionError extends Error {}\\n\\nclass UserService {\\n\\n async listUsers(applicant: User): Promise {\\n if (!ctx.user.isAdmin) {\\n // Use exception here.\\n throw new PermissionError();\\n }\\n\\n return User.find({ org: user.org });\\n }\\n\\n}\\n\\n// This hook measures the execution time and the controller method and hooks.\\n@Hook(() => {\\n const time = process.hrtime();\\n\\n // This post function will still be executed\\n // even if an error is thrown in listUsers.\\n return () => {\\n const seconds = process.hrtime(time)[0];\\n console.log(`Executed in ${seconds} seconds`);\\n };\\n})\\nexport class AppController {\\n\\n @dependency\\n users: UserService;\\n\\n @Get(\'/users\')\\n @UseSessions({ user: fetchUser(User) })\\n @UserRequired()\\n listUsers(ctx: Context) {\\n return new HttpResponseOK(\\n await users.listUsers(ctx.user)\\n );\\n }\\n\\n handleError(error: Error, ctx: Context) {\\n // Converts the exception to an HTTP response.\\n // The error can have been thrown in a service used by the controller.\\n if (error instanceof PermissionError) {\\n return new HttpResponseForbidden();\\n }\\n\\n // Returns an HttpResponseInternalServerError.\\n return renderError(error, response);\\n }\\n}\\n```\\n\\n## Accessing File Metadata during Uploads\\n\\nWhen using the `@ValidateMultipartFormDataBody` hook to handle file upload, it is now possible to access the file metadata.\\n\\n*Example*\\n```typescript\\nexport class UserController {\\n\\n @Post(\'/profile\')\\n @ValidateMultipartFormDataBody({\\n files: {\\n profile: { required: true },\\n }\\n })\\n uploadProfilePhoto(ctx: Context) {\\n const file = ctx.request.body.files.profile;\\n // file.mimeType, file.buffer\\n }\\n\\n}\\n```\\n\\n| Property name | Type | Description |\\n| --- | --- | --- |\\n| `encoding` | `string` | Encoding type of the file |\\n| `filename` | `string\\\\|undefined` | Name of the file on the user\'s computer |\\n| `mimeType` | `string` | Mime type of the file |\\n| `path` | `string` | Path where the file has been saved. If the `saveTo` option was not provided, the value is an empty string. |\\n| `buffer` | `Buffer` | Buffer containing the entire file. If the `saveTo` option was provided, the value is an empty buffer. |"},{"id":"/2021/02/03/version-2.1-release-notes","metadata":{"permalink":"/blog/2021/02/03/version-2.1-release-notes","editUrl":"https://github.com/FoalTS/foal/edit/master/docs/blog/2021-02-03-version-2.1-release-notes.md","source":"@site/blog/2021-02-03-version-2.1-release-notes.md","title":"Version 2.1 release notes","description":"Banner","date":"2021-02-03T00:00:00.000Z","formattedDate":"February 3, 2021","tags":[{"label":"release","permalink":"/blog/tags/release"}],"readingTime":1.495,"hasTruncateMarker":true,"authors":[{"name":"Lo\xefc Poullain","title":"Creator of FoalTS. Software engineer.","url":"https://loicpoullain.com","imageURL":"https://avatars1.githubusercontent.com/u/13604533?v=4"}],"frontMatter":{"title":"Version 2.1 release notes","author":"Lo\xefc Poullain","author_title":"Creator of FoalTS. Software engineer.","author_url":"https://loicpoullain.com","author_image_url":"https://avatars1.githubusercontent.com/u/13604533?v=4","tags":["release"]},"unlisted":false,"prevItem":{"title":"What\'s new in version 2 (part 1/4)","permalink":"/blog/2021/02/17/whats-new-in-version-2-part-1"}},"content":"![Banner](./assets/version-2.1-is-here/banner.png)\\n\\nVersion 2.1 has been released! Here are the improvements that it brings.\\n\\n\x3c!--truncate--\x3e\\n\\n## New Error Page Design\\n\\nWhen an error is thrown or rejected in development, the server returns an error page with some debugging details. The UI of this page has been improved and it now provides more information.\\n\\n![Error page](./assets/version-2.1-is-here/error-page.png)\\n\\n## New Welcome Page\\n\\nWhen creating a new project, the generated welcome page is also different.\\n\\n![Welcome page](./assets/version-2.1-is-here/welcome-page.png)\\n\\n## CLI exits with code 1 when a command fails\\n\\nThis small improvement is useful when we want to stop a CI pipeline when one of its commands fails.\\n\\n## New `@All` decorator\\n\\nThis decorator handles all requests regardless of the HTTP verb (GET, POST, etc.).\\n\\nIt can be used for example to create a `not found` handler.\\n\\n```typescript\\nimport { All, HttpResponseNotFound } from \'@foal/core\';\\n\\nclass AppController {\\n subControllers = [ ViewController ];\\n\\n @All(\'*\')\\n notFound() {\\n return new HttpResponseNotFound(\'The route you are looking for does not exist.\');\\n }\\n}\\n```\\n\\n## New CSRF option in `@UseSessions` and `@JWT`\\n\\nThis option allows you to override the behavior of the configuration specified globally with the key `settings.session.csrf.enabled` or the key `settings.jwt.csrf.enabled`.\\n\\nIt can be useful for example to disable the CSRF protection on a specific route.\\n\\n```typescript\\nimport { HttpResponseOK, Post, UseSessions } from \'@foal/core\';\\n\\nexport class ApiController {\\n @Post(\'/foo\')\\n @UseSessions({ cookie: true })\\n foo() {\\n // This method has the CSRF protection enabled.\\n return new HttpResponseOK();\\n }\\n\\n @Post(\'/bar\')\\n @UseSessions({ cookie: true, csrf: false })\\n bar() {\\n // This method does not have the CSRF protection enabled.\\n return new HttpResponseOK();\\n }\\n}\\n\\n```\\n\\n## Support of `better-sqlite3`\\n\\nWhen using Foal with SQLite, you now have the choice between two drivers: `sqlite3` and `better-sqlite3`. The package `better-sqlite3` is used by default in new projects starting from this version on."}]}')}}]); \ No newline at end of file diff --git a/assets/js/b31df0b0.d229b33f.js b/assets/js/b31df0b0.bb8d6fbc.js similarity index 78% rename from assets/js/b31df0b0.d229b33f.js rename to assets/js/b31df0b0.bb8d6fbc.js index 0c051fc0ea..33c1d353f9 100644 --- a/assets/js/b31df0b0.d229b33f.js +++ b/assets/js/b31df0b0.bb8d6fbc.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1225],{13069:e=>{e.exports=JSON.parse('{"permalink":"/blog/tags/release/page/3","page":3,"postsPerPage":10,"totalPages":3,"totalCount":24,"previousPage":"/blog/tags/release/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1225],{13069:e=>{e.exports=JSON.parse('{"permalink":"/blog/tags/release/page/3","page":3,"postsPerPage":10,"totalPages":3,"totalCount":25,"previousPage":"/blog/tags/release/page/2","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/assets/js/b9d33746.0494621d.js b/assets/js/b9d33746.0494621d.js deleted file mode 100644 index 81c610981d..0000000000 --- a/assets/js/b9d33746.0494621d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4083],{41864:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>i,contentTitle:()=>r,default:()=>a,frontMatter:()=>t,metadata:()=>l,toc:()=>d});var c=s(74848),o=s(28453);const t={title:"Task Scheduling"},r=void 0,l={id:"common/task-scheduling",title:"Task Scheduling",description:"You can schedule jobs using shell scripts and the node-schedule library.",source:"@site/docs/common/task-scheduling.md",sourceDirName:"common",slug:"/common/task-scheduling",permalink:"/docs/common/task-scheduling",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/task-scheduling.md",tags:[],version:"current",frontMatter:{title:"Task Scheduling"},sidebar:"someSidebar",previous:{title:"Logging",permalink:"/docs/common/logging"},next:{title:"REST API",permalink:"/docs/common/rest-blueprints"}},i={},d=[{value:"Example",id:"example",level:2},{value:"Background Jobs with pm2",id:"background-jobs-with-pm2",level:2}];function h(e){const n={a:"a",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsxs)(n.p,{children:["You can schedule jobs using ",(0,c.jsx)(n.a,{href:"/docs/cli/shell-scripts",children:"shell scripts"})," and the ",(0,c.jsx)(n.a,{href:"https://www.npmjs.com/package/node-schedule",children:"node-schedule"})," library."]}),"\n",(0,c.jsx)(n.h2,{id:"example",children:"Example"}),"\n",(0,c.jsx)(n.p,{children:(0,c.jsx)(n.em,{children:"scripts/fetch-metrics.ts"})}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-typescript",children:"export function main(args: any) {\n // Do some stuff\n}\n\n"})}),"\n",(0,c.jsx)(n.p,{children:(0,c.jsx)(n.em,{children:"scripts/schedule-jobs.ts"})}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-typescript",children:"// 3p\nimport { scheduleJob } from 'node-schedule';\nimport { main as fetchMetrics } from './fetch-metrics';\n\nexport async function main(args: any) {\n console.log('Scheduling the job...');\n\n // Run the fetch-metrics script every day at 10:00 AM.\n scheduleJob(\n { hour: 10, minute: 0 },\n () => fetchMetrics(args)\n );\n\n console.log('Job scheduled!');\n}\n\n"})}),"\n",(0,c.jsx)(n.p,{children:"Schedule the job(s):"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-sh",children:"npm run build\nfoal run schedule-jobs arg1=value1\n"})}),"\n",(0,c.jsx)(n.h2,{id:"background-jobs-with-pm2",children:"Background Jobs with pm2"}),"\n",(0,c.jsxs)(n.p,{children:["While the above command works, it does not run the scheduler and the jobs in the background. To do this, you can use ",(0,c.jsx)(n.a,{href:"http://pm2.keymetrics.io/",children:"pm2"}),", a popular process manager for Node.js."]}),"\n",(0,c.jsxs)(n.p,{children:["First you need to install ",(0,c.jsx)(n.em,{children:"locally"})," the Foal CLI:"]}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{children:"npm install @foal/cli\n"})}),"\n",(0,c.jsx)(n.p,{children:"Then you can run the scheduler like this:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-sh",children:"pm2 start ./node_modules/.bin/foal --name scheduler -- run schedule-jobs arg1=value1\n"})}),"\n",(0,c.jsx)(n.p,{children:"If everything works fine, you should see your scheduler running with this command:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{className:"language-sh",children:"pm2 ls\n"})}),"\n",(0,c.jsx)(n.p,{children:"To display the logs of the scheduler and the jobs, use this one:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{children:"pm2 logs scheduler\n"})}),"\n",(0,c.jsx)(n.p,{children:"Eventually, to stop the scheduler and the jobs, you can use this command:"}),"\n",(0,c.jsx)(n.pre,{children:(0,c.jsx)(n.code,{children:"pm2 delete scheduler\n"})})]})}function a(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,c.jsx)(n,{...e,children:(0,c.jsx)(h,{...e})}):h(e)}},28453:(e,n,s)=>{s.d(n,{R:()=>r,x:()=>l});var c=s(96540);const o={},t=c.createContext(o);function r(e){const n=c.useContext(t);return c.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),c.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/bd8f4650.0f992ff2.js b/assets/js/bd8f4650.0f992ff2.js new file mode 100644 index 0000000000..8f9264a1ac --- /dev/null +++ b/assets/js/bd8f4650.0f992ff2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1262],{72929:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var i=t(74848),o=t(28453);const s={title:"Installation",id:"tuto-1-installation",slug:"1-installation"},r=void 0,l={id:"tutorials/simple-todo-list/tuto-1-installation",title:"Installation",description:"In this tutorial you will learn how to create a basic web application with FoalTS. The demo application is a simple to-do list with which users can view, create and delete their tasks.",source:"@site/docs/tutorials/simple-todo-list/1-installation.md",sourceDirName:"tutorials/simple-todo-list",slug:"/tutorials/simple-todo-list/1-installation",permalink:"/docs/tutorials/simple-todo-list/1-installation",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/simple-todo-list/1-installation.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Installation",id:"tuto-1-installation",slug:"1-installation"},sidebar:"someSidebar",previous:{title:"Introduction",permalink:"/docs/"},next:{title:"Introduction",permalink:"/docs/tutorials/simple-todo-list/2-introduction"}},a={},c=[{value:"Create a New Project",id:"create-a-new-project",level:2},{value:"Start The Server",id:"start-the-server",level:2}];function d(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"In this tutorial you will learn how to create a basic web application with FoalTS. The demo application is a simple to-do list with which users can view, create and delete their tasks."}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Requirements:"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://nodejs.org/en/",children:"Node.js"})," 18 or greater"]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"create-a-new-project",children:"Create a New Project"}),"\n",(0,i.jsxs)(n.p,{children:["First you need to install globaly the ",(0,i.jsx)(n.em,{children:"Command Line Interface (CLI)"})," of FoalTS. It will help you create a new project and generate files all along your development."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sh",children:"npm install -g @foal/cli\n"})}),"\n",(0,i.jsx)(n.p,{children:"Then create a new application."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sh",children:"npx @foal/cli createapp my-app\n"})}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["Having trouble installing Foal? \ud83d\udc49 Checkout our ",(0,i.jsx)(n.a,{href:"./installation-troubleshooting",children:"troubleshooting page"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["This command generates a new directory with the basic structure of the new application. It also installs all the dependencies. Let's look at what ",(0,i.jsx)(n.code,{children:"createapp"})," created:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"my-app/\n config/\n node_modules/\n public/\n src/\n app/\n e2e/\n scripts/\n package.json\n tsconfig.*.json\n .eslintrc.js\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The outer ",(0,i.jsx)(n.code,{children:"my-app"})," root directory is just a container for your project."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"config/"})," directory contains configuration files for your different environments (production, test, development, e2e, etc)."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"node_modules/"})," directory contains all the prod and dev dependencies of your project."]}),"\n",(0,i.jsxs)(n.li,{children:["The static files are located in the ",(0,i.jsx)(n.code,{children:"public/"})," directory. They are usually images, CSS and client JavaScript files and are served directly when the server is running."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"src/"})," directory contains all the source code of the application.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The inner ",(0,i.jsx)(n.code,{children:"app/"})," directory includes the components of your server (controllers, services and hooks)."]}),"\n",(0,i.jsxs)(n.li,{children:["End-to-end tests are located in the ",(0,i.jsx)(n.code,{children:"e2e/"})," directory."]}),"\n",(0,i.jsxs)(n.li,{children:["The inner ",(0,i.jsx)(n.code,{children:"scripts/"})," folder contains scripts intended to be called from the command line (ex: create-user)."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"package.json"})," lists the dependencies and commands of the project."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"tsconfig.*.json"})," files list the TypeScript compiler configuration for each ",(0,i.jsx)(n.code,{children:"npm"})," command."]}),"\n",(0,i.jsxs)(n.li,{children:["Finally the linting configuration can be found in the ",(0,i.jsx)(n.code,{children:".eslintrc.js"})," file."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"TypeScript"})}),"\n",(0,i.jsxs)(n.p,{children:["The language used to develop a FoalTS application is ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/",children:"TypeScript"}),". It is a typed superset of JavaScript that compiles to plain JavaScript. The benefits of using TypeScript are many, but in summary, the language provides great tools and the future features of JavaScript."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"start-the-server",children:"Start The Server"}),"\n",(0,i.jsx)(n.p,{children:"Let's verify that the FoalTS project works. Run the following commands:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"cd my-app\nnpm run dev\n"})}),"\n",(0,i.jsx)(n.p,{children:"You've started the development server."}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.strong,{children:"development server"})," watches at your files and automatically compiles and reloads your code. You don\u2019t need to restart the server each time you make code changes. Note that it is only intended to be used in development, do not use it on production."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Port 3001 already in use?"})}),"\n",(0,i.jsxs)(n.p,{children:["You can define in ",(0,i.jsx)(n.code,{children:"config/default.json"})," which port the application is using."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Go to ",(0,i.jsx)(n.a,{href:"http://localhost:3001",children:"http://localhost:3001"})," in your browser. You should see the text ",(0,i.jsx)(n.em,{children:"Welcome on board"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Congratulations, you now have a server running!"})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var i=t(96540);const o={},s=i.createContext(o);function r(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/bd8f4650.3a8cca54.js b/assets/js/bd8f4650.3a8cca54.js deleted file mode 100644 index 62d68acbc7..0000000000 --- a/assets/js/bd8f4650.3a8cca54.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1262],{72929:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var i=t(74848),o=t(28453);const s={title:"Installation",id:"tuto-1-installation",slug:"1-installation"},r=void 0,l={id:"tutorials/simple-todo-list/tuto-1-installation",title:"Installation",description:"In this tutorial you will learn how to create a basic web application with FoalTS. The demo application is a simple to-do list with which users can view, create and delete their tasks.",source:"@site/docs/tutorials/simple-todo-list/1-installation.md",sourceDirName:"tutorials/simple-todo-list",slug:"/tutorials/simple-todo-list/1-installation",permalink:"/docs/tutorials/simple-todo-list/1-installation",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/simple-todo-list/1-installation.md",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Installation",id:"tuto-1-installation",slug:"1-installation"},sidebar:"someSidebar",previous:{title:"Introduction",permalink:"/docs/"},next:{title:"Introduction",permalink:"/docs/tutorials/simple-todo-list/2-introduction"}},a={},c=[{value:"Create a New Project",id:"create-a-new-project",level:2},{value:"Start The Server",id:"start-the-server",level:2}];function d(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.p,{children:"In this tutorial you will learn how to create a basic web application with FoalTS. The demo application is a simple to-do list with which users can view, create and delete their tasks."}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Requirements:"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.a,{href:"https://nodejs.org/en/",children:"Node.js"})," 18 or greater"]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"create-a-new-project",children:"Create a New Project"}),"\n",(0,i.jsxs)(n.p,{children:["First you need to install globaly the ",(0,i.jsx)(n.em,{children:"Command Line Interface (CLI)"})," of FoalTS. It will help you create a new project and generate files all along your development."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sh",children:"npm install -g @foal/cli\n"})}),"\n",(0,i.jsx)(n.p,{children:"Then create a new application."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-sh",children:"foal createapp my-app\n"})}),"\n",(0,i.jsx)(n.admonition,{type:"note",children:(0,i.jsxs)(n.p,{children:["Having trouble installing Foal? \ud83d\udc49 Checkout our ",(0,i.jsx)(n.a,{href:"./installation-troubleshooting",children:"troubleshooting page"}),"."]})}),"\n",(0,i.jsxs)(n.p,{children:["This command generates a new directory with the basic structure of the new application. It also installs all the dependencies. Let's look at what ",(0,i.jsx)(n.code,{children:"createapp"})," created:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-shell",children:"my-app/\n config/\n node_modules/\n public/\n src/\n app/\n e2e/\n scripts/\n package.json\n tsconfig.*.json\n .eslintrc.js\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The outer ",(0,i.jsx)(n.code,{children:"my-app"})," root directory is just a container for your project."]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"config/"})," directory contains configuration files for your different environments (production, test, development, e2e, etc)."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"node_modules/"})," directory contains all the prod and dev dependencies of your project."]}),"\n",(0,i.jsxs)(n.li,{children:["The static files are located in the ",(0,i.jsx)(n.code,{children:"public/"})," directory. They are usually images, CSS and client JavaScript files and are served directly when the server is running."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"src/"})," directory contains all the source code of the application.","\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:["The inner ",(0,i.jsx)(n.code,{children:"app/"})," directory includes the components of your server (controllers, services and hooks)."]}),"\n",(0,i.jsxs)(n.li,{children:["End-to-end tests are located in the ",(0,i.jsx)(n.code,{children:"e2e/"})," directory."]}),"\n",(0,i.jsxs)(n.li,{children:["The inner ",(0,i.jsx)(n.code,{children:"scripts/"})," folder contains scripts intended to be called from the command line (ex: create-user)."]}),"\n"]}),"\n"]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"package.json"})," lists the dependencies and commands of the project."]}),"\n",(0,i.jsxs)(n.li,{children:["The ",(0,i.jsx)(n.code,{children:"tsconfig.*.json"})," files list the TypeScript compiler configuration for each ",(0,i.jsx)(n.code,{children:"npm"})," command."]}),"\n",(0,i.jsxs)(n.li,{children:["Finally the linting configuration can be found in the ",(0,i.jsx)(n.code,{children:".eslintrc.js"})," file."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"TypeScript"})}),"\n",(0,i.jsxs)(n.p,{children:["The language used to develop a FoalTS application is ",(0,i.jsx)(n.a,{href:"https://www.typescriptlang.org/",children:"TypeScript"}),". It is a typed superset of JavaScript that compiles to plain JavaScript. The benefits of using TypeScript are many, but in summary, the language provides great tools and the future features of JavaScript."]}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"start-the-server",children:"Start The Server"}),"\n",(0,i.jsx)(n.p,{children:"Let's verify that the FoalTS project works. Run the following commands:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"cd my-app\nnpm run dev\n"})}),"\n",(0,i.jsx)(n.p,{children:"You've started the development server."}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.strong,{children:"development server"})," watches at your files and automatically compiles and reloads your code. You don\u2019t need to restart the server each time you make code changes. Note that it is only intended to be used in development, do not use it on production."]}),"\n"]}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.strong,{children:"Port 3001 already in use?"})}),"\n",(0,i.jsxs)(n.p,{children:["You can define in ",(0,i.jsx)(n.code,{children:"config/default.json"})," which port the application is using."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Go to ",(0,i.jsx)(n.a,{href:"http://localhost:3001",children:"http://localhost:3001"})," in your browser. You should see the text ",(0,i.jsx)(n.em,{children:"Welcome on board"}),"."]}),"\n",(0,i.jsx)(n.p,{children:"Congratulations, you now have a server running!"})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>l});var i=t(96540);const o={},s=i.createContext(o);function r(e){const n=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/c7c1656f.96c62f06.js b/assets/js/c7c1656f.c0d5f7b2.js similarity index 53% rename from assets/js/c7c1656f.96c62f06.js rename to assets/js/c7c1656f.c0d5f7b2.js index 6e00035c53..1e8a306697 100644 --- a/assets/js/c7c1656f.96c62f06.js +++ b/assets/js/c7c1656f.c0d5f7b2.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5426],{22847:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>d});var s=n(74848),o=n(28453);const r={title:"Logging Users In and Out",id:"tuto-8-authentication",slug:"8-authentication"},i=void 0,a={id:"tutorials/real-world-example-with-react/tuto-8-authentication",title:"Logging Users In and Out",description:"Stories are displayed on the home page. If we want users to be able to post new stories and upload a profile picture, we need to allow them to log in to the application.",source:"@site/docs/tutorials/real-world-example-with-react/8-authentication.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/8-authentication",permalink:"/docs/tutorials/real-world-example-with-react/8-authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/8-authentication.md",tags:[],version:"current",sidebarPosition:8,frontMatter:{title:"Logging Users In and Out",id:"tuto-8-authentication",slug:"8-authentication"},sidebar:"someSidebar",previous:{title:"The Frontend App",permalink:"/docs/tutorials/real-world-example-with-react/7-add-frontend"},next:{title:"Authenticating Users in the API",permalink:"/docs/tutorials/real-world-example-with-react/9-authenticated-api"}},l={},d=[];function c(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.p,{children:"Stories are displayed on the home page. If we want users to be able to post new stories and upload a profile picture, we need to allow them to log in to the application."}),"\n",(0,s.jsx)(t.p,{children:"To do this, we will use Foal's sessions with cookies."}),"\n",(0,s.jsxs)(t.blockquote,{children:["\n",(0,s.jsxs)(t.p,{children:["FoalTS offers many options for user authentication. For example, you can send session tokens with the ",(0,s.jsx)(t.code,{children:"Authorization"})," header or use stateless tokens with JWT. We won't explore all these possibilities in this tutorial but you can find the full documentation ",(0,s.jsx)(t.a,{href:"/docs/authentication/quick-start",children:"here"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["Open the file ",(0,s.jsx)(t.code,{children:"api.controller.ts"})," and add the ",(0,s.jsx)(t.code,{children:"@UseSessions"})," hook at the top of the class."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"import { ApiInfo, ApiServer, controller, UseSessions } from '@foal/core';\nimport { User } from '../entities';\nimport { StoriesController } from './api';\n\n@ApiInfo({\n title: 'Application API',\n version: '1.0.0'\n})\n@ApiServer({\n url: '/api'\n})\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n subControllers = [\n controller('/stories', StoriesController),\n ];\n\n}\n\n"})}),"\n",(0,s.jsxs)(t.p,{children:["When used with the ",(0,s.jsx)(t.code,{children:"cookie"})," option, this hook ensures that ",(0,s.jsx)(t.code,{children:"ctx.session"})," is always defined in every method of the controller and its subcontrollers. This object can be used to store information between multiple requests, such as a user ID for example. You will use it to authenticate users."]}),"\n",(0,s.jsxs)(t.blockquote,{children:["\n",(0,s.jsxs)(t.p,{children:["In the background, Foal generates a unique session token for each user using the API and stores it in a cookie on the client host. When the client makes a new request, the browser automatically sends the token with the request so that the server can retrieve the session information. The session data is stored in the database in the ",(0,s.jsx)(t.em,{children:"sessions"})," table."]}),"\n",(0,s.jsx)(t.p,{children:"But you don't need to worry about it, everything is managed by Foal."}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"Create a new controller."}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"foal generate controller api/auth --register\n"})}),"\n",(0,s.jsx)(t.p,{children:"Open the new created file and add two routes."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"API endpoint"}),(0,s.jsx)(t.th,{children:"Method"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"/api/auth/login"})}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"POST"})}),(0,s.jsx)(t.td,{children:"Logs the user in. An email and a password are expected in the request body."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"/api/auth/logout"})}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"POST"})}),(0,s.jsx)(t.td,{children:"Logs the user out."})]})]})]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseNoContent, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\nimport { User } from '../../entities';\n\nconst credentialsSchema = {\n type: 'object',\n properties: {\n email: { type: 'string', format: 'email', maxLength: 255 },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n additionalProperties: false,\n};\n\nexport class AuthController {\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const email = ctx.request.body.email;\n const password = ctx.request.body.password;\n\n const user = await User.findOneBy({ email });\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!(await verifyPassword(password, user.password))) {\n return new HttpResponseUnauthorized();\n }\n\n ctx.session!.setUser(user);\n ctx.user = user;\n\n return new HttpResponseOK({\n id: user.id,\n name: user.name,\n });\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n await ctx.session!.destroy();\n return new HttpResponseNoContent();\n }\n\n}\n\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.code,{children:"login"})," method first checks that the user exists and that the credentials provided are correct. If so, it associates the user with the current session."]}),"\n",(0,s.jsxs)(t.p,{children:["On subsequent requests, the ",(0,s.jsx)(t.em,{children:"UseSessions"})," hook will retrieve the user's ID from the session and set the ",(0,s.jsx)(t.code,{children:"ctx.user"})," property accordingly. If the user has not previously logged in, then ",(0,s.jsx)(t.code,{children:"ctx.user"})," will be ",(0,s.jsx)(t.code,{children:"null"}),". If they have, then ",(0,s.jsx)(t.code,{children:"ctx.user"})," will be an instance of ",(0,s.jsx)(t.code,{children:"User"}),". This is made possible by the ",(0,s.jsx)(t.code,{children:"user"})," option we provided to the hook earlier. It is actually the function that takes the user ID as parameter and returns the value to assign to ",(0,s.jsx)(t.code,{children:"ctx.user"}),"."]})]})}function h(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>i,x:()=>a});var s=n(96540);const o={},r=s.createContext(o);function i(e){const t=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5426],{22847:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>d});var s=n(74848),o=n(28453);const r={title:"Logging Users In and Out",id:"tuto-8-authentication",slug:"8-authentication"},i=void 0,a={id:"tutorials/real-world-example-with-react/tuto-8-authentication",title:"Logging Users In and Out",description:"Stories are displayed on the home page. If we want users to be able to post new stories and upload a profile picture, we need to allow them to log in to the application.",source:"@site/docs/tutorials/real-world-example-with-react/8-authentication.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/8-authentication",permalink:"/docs/tutorials/real-world-example-with-react/8-authentication",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/8-authentication.md",tags:[],version:"current",sidebarPosition:8,frontMatter:{title:"Logging Users In and Out",id:"tuto-8-authentication",slug:"8-authentication"},sidebar:"someSidebar",previous:{title:"The Frontend App",permalink:"/docs/tutorials/real-world-example-with-react/7-add-frontend"},next:{title:"Authenticating Users in the API",permalink:"/docs/tutorials/real-world-example-with-react/9-authenticated-api"}},l={},d=[];function c(e){const t={a:"a",blockquote:"blockquote",code:"code",em:"em",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.p,{children:"Stories are displayed on the home page. If we want users to be able to post new stories and upload a profile picture, we need to allow them to log in to the application."}),"\n",(0,s.jsx)(t.p,{children:"To do this, we will use Foal's sessions with cookies."}),"\n",(0,s.jsxs)(t.blockquote,{children:["\n",(0,s.jsxs)(t.p,{children:["FoalTS offers many options for user authentication. For example, you can send session tokens with the ",(0,s.jsx)(t.code,{children:"Authorization"})," header or use stateless tokens with JWT. We won't explore all these possibilities in this tutorial but you can find the full documentation ",(0,s.jsx)(t.a,{href:"/docs/authentication/quick-start",children:"here"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(t.p,{children:["Open the file ",(0,s.jsx)(t.code,{children:"api.controller.ts"})," and add the ",(0,s.jsx)(t.code,{children:"@UseSessions"})," hook at the top of the class."]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"import { ApiInfo, ApiServer, controller, UseSessions } from '@foal/core';\nimport { User } from '../entities';\nimport { StoriesController } from './api';\n\n@ApiInfo({\n title: 'Application API',\n version: '1.0.0'\n})\n@ApiServer({\n url: '/api'\n})\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n subControllers = [\n controller('/stories', StoriesController),\n ];\n\n}\n\n"})}),"\n",(0,s.jsxs)(t.p,{children:["When used with the ",(0,s.jsx)(t.code,{children:"cookie"})," option, this hook ensures that ",(0,s.jsx)(t.code,{children:"ctx.session"})," is always defined in every method of the controller and its subcontrollers. This object can be used to store information between multiple requests, such as a user ID for example. You will use it to authenticate users."]}),"\n",(0,s.jsxs)(t.blockquote,{children:["\n",(0,s.jsxs)(t.p,{children:["In the background, Foal generates a unique session token for each user using the API and stores it in a cookie on the client host. When the client makes a new request, the browser automatically sends the token with the request so that the server can retrieve the session information. The session data is stored in the database in the ",(0,s.jsx)(t.em,{children:"sessions"})," table."]}),"\n",(0,s.jsx)(t.p,{children:"But you don't need to worry about it, everything is managed by Foal."}),"\n"]}),"\n",(0,s.jsx)(t.p,{children:"Create a new controller."}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"npx foal generate controller api/auth --register\n"})}),"\n",(0,s.jsx)(t.p,{children:"Open the new created file and add two routes."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"API endpoint"}),(0,s.jsx)(t.th,{children:"Method"}),(0,s.jsx)(t.th,{children:"Description"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"/api/auth/login"})}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"POST"})}),(0,s.jsx)(t.td,{children:"Logs the user in. An email and a password are expected in the request body."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"/api/auth/logout"})}),(0,s.jsx)(t.td,{children:(0,s.jsx)(t.code,{children:"POST"})}),(0,s.jsx)(t.td,{children:"Logs the user out."})]})]})]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseNoContent, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\nimport { User } from '../../entities';\n\nconst credentialsSchema = {\n type: 'object',\n properties: {\n email: { type: 'string', format: 'email', maxLength: 255 },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n additionalProperties: false,\n};\n\nexport class AuthController {\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const email = ctx.request.body.email;\n const password = ctx.request.body.password;\n\n const user = await User.findOneBy({ email });\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!(await verifyPassword(password, user.password))) {\n return new HttpResponseUnauthorized();\n }\n\n ctx.session!.setUser(user);\n ctx.user = user;\n\n return new HttpResponseOK({\n id: user.id,\n name: user.name,\n });\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n await ctx.session!.destroy();\n return new HttpResponseNoContent();\n }\n\n}\n\n"})}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.code,{children:"login"})," method first checks that the user exists and that the credentials provided are correct. If so, it associates the user with the current session."]}),"\n",(0,s.jsxs)(t.p,{children:["On subsequent requests, the ",(0,s.jsx)(t.em,{children:"UseSessions"})," hook will retrieve the user's ID from the session and set the ",(0,s.jsx)(t.code,{children:"ctx.user"})," property accordingly. If the user has not previously logged in, then ",(0,s.jsx)(t.code,{children:"ctx.user"})," will be ",(0,s.jsx)(t.code,{children:"null"}),". If they have, then ",(0,s.jsx)(t.code,{children:"ctx.user"})," will be an instance of ",(0,s.jsx)(t.code,{children:"User"}),". This is made possible by the ",(0,s.jsx)(t.code,{children:"user"})," option we provided to the hook earlier. It is actually the function that takes the user ID as parameter and returns the value to assign to ",(0,s.jsx)(t.code,{children:"ctx.user"}),"."]})]})}function h(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>i,x:()=>a});var s=n(96540);const o={},r=s.createContext(o);function i(e){const t=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d5913837.1467cc3c.js b/assets/js/d5913837.1467cc3c.js new file mode 100644 index 0000000000..ede99122fb --- /dev/null +++ b/assets/js/d5913837.1467cc3c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[7823],{14989:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>m,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var n=o(74848),r=o(28453);const s={title:"Version 4.5 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.5-release-notes.png",tags:["release"]},a=void 0,i={permalink:"/blog/2024/08/22/version-4.5-release-notes",editUrl:"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-08-22-version-4.5-release-notes.md",source:"@site/blog/2024-08-22-version-4.5-release-notes.md",title:"Version 4.5 release notes",description:"Banner",date:"2024-08-22T00:00:00.000Z",formattedDate:"August 22, 2024",tags:[{label:"release",permalink:"/blog/tags/release"}],readingTime:2.425,hasTruncateMarker:!0,authors:[{name:"Lo\xefc Poullain",title:"Creator of FoalTS. Software engineer.",url:"https://loicpoullain.com",imageURL:"https://avatars1.githubusercontent.com/u/13604533?v=4"}],frontMatter:{title:"Version 4.5 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.5-release-notes.png",tags:["release"]},unlisted:!1,nextItem:{title:"Version 4.4 release notes",permalink:"/blog/2024/04/25/version-4.4-release-notes"}},l={authorsImageUrls:[void 0]},c=[];function u(e){const t={a:"a",img:"img",p:"p",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Banner",src:o(60572).A+"",width:"914",height:"315"})}),"\n",(0,n.jsxs)(t.p,{children:["Version 4.5 of ",(0,n.jsx)(t.a,{href:"https://foalts.org/",children:"Foal"})," is out!"]})]})}function m(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(u,{...e})}):u(e)}},60572:(e,t,o)=>{o.d(t,{A:()=>n});const n=o.p+"assets/images/banner-3f2086c61f3c010fc38db9701cd3d398.png"},28453:(e,t,o)=>{o.d(t,{R:()=>a,x:()=>i});var n=o(96540);const r={},s=n.createContext(r);function a(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d7767d70.692fe249.js b/assets/js/d7767d70.b916093c.js similarity index 55% rename from assets/js/d7767d70.692fe249.js rename to assets/js/d7767d70.b916093c.js index e76a860def..66877300bd 100644 --- a/assets/js/d7767d70.692fe249.js +++ b/assets/js/d7767d70.b916093c.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[6491],{55066:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var t=s(74848),o=s(28453),i=s(11470),r=s(19365);const a={title:"Session Tokens",sidebar_label:"Session Tokens"},c=void 0,l={id:"authentication/session-tokens",title:"Session Tokens",description:"Introduction",source:"@site/docs/authentication/session-tokens.md",sourceDirName:"authentication",slug:"/authentication/session-tokens",permalink:"/docs/authentication/session-tokens",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/session-tokens.md",tags:[],version:"current",frontMatter:{title:"Session Tokens",sidebar_label:"Session Tokens"},sidebar:"someSidebar",previous:{title:"Passwords",permalink:"/docs/authentication/password-management"},next:{title:"JSON Web Tokens",permalink:"/docs/authentication/jwt"}},d={},h=[{value:"Introduction",id:"introduction",level:2},{value:"The Basics",id:"the-basics",level:2},{value:"Choosing a session store",id:"choosing-a-session-store",level:3},{value:"TypeORMStore",id:"typeormstore",level:4},{value:"RedisStore",id:"redisstore",level:4},{value:"MongoDBStore",id:"mongodbstore",level:4},{value:"Usage with the Authorization header",id:"usage-with-the-authorization-header",level:3},{value:"Usage with cookies",id:"usage-with-cookies",level:3},{value:"Adding authentication and access control",id:"adding-authentication-and-access-control",level:3},{value:"Destroying the session",id:"destroying-the-session",level:3},{value:"Reading User Information on the Client (cookies)",id:"reading-user-information-on-the-client-cookies",level:2},{value:"Save and Read Content",id:"save-and-read-content",level:2},{value:"Flash Content",id:"flash-content",level:3},{value:"Security",id:"security",level:2},{value:"Session Expiration Timeouts",id:"session-expiration-timeouts",level:3},{value:"Revoking Sessions",id:"revoking-sessions",level:3},{value:"Revoking One Session",id:"revoking-one-session",level:4},{value:"Revoking All Sessions",id:"revoking-all-sessions",level:4},{value:"Query All Sessions of a User",id:"query-all-sessions-of-a-user",level:3},{value:"Query All Connected Users",id:"query-all-connected-users",level:3},{value:"Force the Disconnection of a User",id:"force-the-disconnection-of-a-user",level:3},{value:"Re-generate the Session ID",id:"re-generate-the-session-id",level:3},{value:"Advanced",id:"advanced",level:2},{value:"Specify the Store Locally",id:"specify-the-store-locally",level:3},{value:"Cleanup Expired Sessions",id:"cleanup-expired-sessions",level:3},{value:"Implement a Custom Store",id:"implement-a-custom-store",level:3},{value:"Usage with Cookies",id:"usage-with-cookies-1",level:3},{value:"Do not Auto-Create the Session",id:"do-not-auto-create-the-session",level:4},{value:"Override the Cookie Options",id:"override-the-cookie-options",level:4},{value:"Require the Cookie",id:"require-the-cookie",level:4},{value:"Read a Session From a Token",id:"read-a-session-from-a-token",level:3},{value:"Save Manually a Session",id:"save-manually-a-session",level:3},{value:"Provide A Custom Client to Use in the Stores",id:"provide-a-custom-client-to-use-in-the-stores",level:3},{value:"RedisStore",id:"redisstore-1",level:4},{value:"MongoDBStore",id:"mongodbstore-1",level:4}];function u(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This document assumes that you have alread read the ",(0,t.jsx)(n.a,{href:"/docs/authentication/quick-start",children:"Quick Start"})," page."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In FoalTS, web sessions are temporary states that can be associated with a specific user. They are identified by a token and are mainly used to keep users authenticated between several HTTP requests (the client sends the token on each request to authenticate the user)."}),"\n",(0,t.jsx)(n.p,{children:"A session usually begins when the user logs in (or starts visiting the website) and ends after a period of inactivity or when the user logs out. By inactivity, we mean that the server no longer receives requests from the authenticated user for a certain period of time."}),"\n",(0,t.jsx)(n.h2,{id:"the-basics",children:"The Basics"}),"\n",(0,t.jsx)(n.h3,{id:"choosing-a-session-store",children:"Choosing a session store"}),"\n",(0,t.jsxs)(n.p,{children:["To begin, you must first specify where the session states will be stored. FoalTS provides several ",(0,t.jsx)(n.em,{children:"session stores"})," for this. For example, you can use the ",(0,t.jsx)(n.code,{children:"TypeORMStore"})," to save the sessions in your SQL database or the ",(0,t.jsx)(n.code,{children:"RedisStore"})," to save them in a redis cache."]}),"\n",(0,t.jsxs)(n.p,{children:["To do so, the package name of the store must be provided with the configuration key ",(0,t.jsx)(n.code,{children:"settings.session.store"}),"."]}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:'settings:\n session:\n store: "@foal/typeorm"\n'})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/typeorm",\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/typeorm",\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.h4,{id:"typeormstore",children:"TypeORMStore"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install typeorm@0.3.17 @foal/typeorm\n"})}),"\n",(0,t.jsxs)(n.p,{children:["This store uses the default TypeORM connection whose configuration is usually specified in ",(0,t.jsx)(n.code,{children:"config/default.{json|yml|js}"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Session states are saved in the ",(0,t.jsx)(n.code,{children:"databasesession"})," table of your SQL database. In order to create it, you need to add and run migrations. For this purpose, you can export the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity in your user file and execute the following commands."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"entities/user.entity.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { DatabaseSession } from '@foal/typeorm';\nimport { BaseEntity, Entity } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n /* ... */\n}\n\nexport { DatabaseSession }\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Warning"}),": If you use TypeORM store, then your entity IDs must be numbers (not strings)."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"redisstore",children:"RedisStore"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install @foal/redis\n"})}),"\n",(0,t.jsx)(n.p,{children:"In order to use this store, you must provide the redis URI in the configuration."}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n store: \"@foal/redis\"\n redis:\n uri: 'redis://localhost:6379'\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/redis",\n },\n "redis": {\n "uri": "redis://localhost:6379"\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/redis",\n },\n redis: {\n uri: "redis://localhost:6379"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If you are using the redis store, please ensure that you have a ",(0,t.jsx)(n.code,{children:"@dependency store: Store"})," property in at least one of your controllers or services. Otherwise, the connection to the redis database will not be established when the application starts."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"mongodbstore",children:"MongoDBStore"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install @foal/mongodb\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This package uses the ",(0,t.jsx)(n.a,{href:"https://www.npmjs.com/package/mongodb",children:"mongodb Node.JS driver"})," which uses the ",(0,t.jsx)(n.a,{href:"https://www.npmjs.com/package/@types/node",children:"@types/node"})," package under the hood. If you get type compilation errors, try to upgrade this dependency."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["This store saves your session states in a MongoDB database (using the collection ",(0,t.jsx)(n.code,{children:"sessions"}),"). In order to use it, you must provide the MongoDB URI in the configuration."]}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n store: \"@foal/mongodb\"\n mongodb:\n uri: 'mongodb://localhost:27017'\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/mongodb",\n },\n "mongodb": {\n "uri": "mongodb://localhost:27017"\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/mongodb",\n },\n mongodb: {\n uri: "mongodb://localhost:27017"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If you are using the MongoDB store, please ensure that you have a ",(0,t.jsx)(n.code,{children:"@dependency store: Store"})," property in at least one of your controllers or services. Otherwise, the connection to the MondoDB database will not be established when the application starts."]}),"\n"]}),"\n",(0,t.jsxs)(n.h3,{id:"usage-with-the-authorization-header",children:["Usage with the ",(0,t.jsx)(n.code,{children:"Authorization"})," header"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This section explains how to use sessions with a ",(0,t.jsx)(n.code,{children:"bearer"})," token and the ",(0,t.jsx)(n.code,{children:"Authorization"})," header. See the section below to see how to use them with cookies."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"The mechanism is as follows:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Upon login, create the session and assign it to ",(0,t.jsx)(n.code,{children:"ctx.session"}),". Then return the session token in the response."]}),"\n",(0,t.jsxs)(n.li,{children:["On subsequent requests, send the token in the ",(0,t.jsx)(n.code,{children:"Authorization"})," header with this scheme: ",(0,t.jsx)(n.code,{children:"Authorization: Bearer "}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\n@UseSessions()\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n async login(ctx: Context) {\n // Check the user credentials...\n\n ctx.session = await createSession(this.store);\n\n // See the \"authentication\" section below\n // to see how to associate a user to the session.\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n // If the request has an Authorization header with a valid token\n // then ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If the ",(0,t.jsx)(n.code,{children:"Authorization"})," header does not use the ",(0,t.jsx)(n.code,{children:"bearer"})," scheme or if the token is invalid or expired, then the hook returns a 400 or 401 error."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["If you want to make sure that ",(0,t.jsx)(n.code,{children:"ctx.session"})," is set and get a 400 error if no ",(0,t.jsx)(n.code,{children:"Authorization"})," header is provided, you can use the ",(0,t.jsx)(n.code,{children:"required"})," option for this."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n @UseSessions()\n async login(ctx: Context) {\n // Check the user credentials...\n\n ctx.session = await createSession(this.store);\n\n // See the \"authentication\" section below\n // to see how to associate a user to the session.\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n @UseSessions({ required: true })\n readProducts(ctx: Context) {\n // ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"usage-with-cookies",children:"Usage with cookies"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This section explains how to use sessions with cookies. See the section above to see how to use them with a ",(0,t.jsx)(n.code,{children:"bearer"})," token and the ",(0,t.jsx)(n.code,{children:"Authorization"})," header."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Be aware that if you use cookies, your application must provide a ",(0,t.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF defense"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["When using the ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook with the ",(0,t.jsx)(n.code,{children:"cookie"})," option, FoalTS makes sure that ",(0,t.jsx)(n.code,{children:"ctx.session"})," is always set and takes care of managing the session token on the client (using a cookie)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\n@UseSessions({ cookie: true })\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n login(ctx: Context) {\n // Check the user credentials...\n\n // See the \"authentication\" section below\n // to see how to associate a user to the session.\n\n return new HttpResponseOK();\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n // ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If the session has expired, the hook returns a 401 error. If you want to redirect the user to the login page, you can use the ",(0,t.jsx)(n.code,{children:"redirectTo"})," option to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@UseSessions({\n cookie: true,\n redirectTo: '/login'\n})\nexport class ApiController {\n\n @Get('/products')\n readProducts(ctx: Context) {\n // ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-authentication-and-access-control",children:"Adding authentication and access control"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This section explains how to associate a specific user to a session and how to use ",(0,t.jsx)(n.code,{children:"ctx.user"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Sessions can be used to authenticate users. To do this, you can use the ",(0,t.jsx)(n.code,{children:"Session.setUser"})," method and the ",(0,t.jsx)(n.code,{children:"user"})," option of ",(0,t.jsx)(n.code,{children:"@UseSessions"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n@UseSessions({\n // If the session is attached to a user,\n // then retrieve the user from the database\n // and assign it to ctx.user\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n async login(ctx: Context) {\n // Check the user credentials...\n // const user = ...\n\n ctx.session = await createSession(this.store);\n\n // Attach the user to the session.\n ctx.session.setUser(user);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n // If the ctx.session is defined and the session is attached to a user\n // then ctx.user is an instance of User. Otherwise it is null.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If you want to restrict certain routes to authenticated users, you can use the ",(0,t.jsx)(n.code,{children:"@UserRequired"})," hook for this."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK, UserRequired, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n@UseSessions({\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n @Get('/products')\n @UserRequired()\n readProducts(ctx: Context) {\n // ctx.user is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If the user is not authenticated, the hook returns a 401 error. If you want to redirect the user to the login page, you can use the ",(0,t.jsx)(n.code,{children:"redirectTo"})," option to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK, UserRequired, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n@UseSessions({\n redirectTo: '/login',\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n @Get('/products')\n @UserRequired({\n redirectTo: '/login'\n })\n readProducts(ctx: Context) {\n // ctx.user is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"destroying-the-session",children:"Destroying the session"}),"\n",(0,t.jsxs)(n.p,{children:["Sessions can be destroyed (i.e users can be logged out) using their ",(0,t.jsx)(n.code,{children:"destroy"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseNoContent, Post, UseSessions } from '@foal/core';\n\nexport class AuthController {\n\n @Post('/logout')\n @UseSessions()\n async logout(ctx: Context) {\n if (ctx.session) {\n await ctx.session.destroy();\n }\n return new HttpResponseNoContent();\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"reading-user-information-on-the-client-cookies",children:"Reading User Information on the Client (cookies)"}),"\n",(0,t.jsxs)(n.p,{children:["When building a SPA with cookie-based authentication, it can sometimes be difficult to know if the user is logged in or to obtain certain information about the user (",(0,t.jsx)(n.code,{children:"isAdmin"}),", etc)."]}),"\n",(0,t.jsxs)(n.p,{children:["Since the authentication token is stored in a cookie with the ",(0,t.jsx)(n.code,{children:"httpOnly"})," directive set to ",(0,t.jsx)(n.code,{children:"true"})," (to mitigate XSS attacks), the front-end application has no way of knowing if a user is logged in, except by making an additional request to the server."]}),"\n",(0,t.jsxs)(n.p,{children:["To solve this problem, Foal provides an option called ",(0,t.jsx)(n.code,{children:"userCookie"})," that allows you to set an additional cookie that the frontend can read with the content you choose. This cookie is synchronized with the session and is refreshed at each request and destroyed when the session expires or when the user logs out."]}),"\n",(0,t.jsxs)(n.p,{children:["In the following example, the ",(0,t.jsx)(n.code,{children:"user"})," cookie is empty if no user is logged in or contains certain information about him/her otherwise. This is particularly useful if you need to display UI elements based on user characteristics."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Server-side code"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"function userToJSON(user: User|null) {\n if (!user) {\n return 'null';\n }\n\n return JSON.stringify({\n email: user.email,\n isAdmin: user.isAdmin\n });\n}\n\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n userCookie: (ctx, services) => userToJSON(ctx.user as User|null)\n})\nexport class ApiController {\n\n @Get('/products')\n @UserRequired()\n async readProducts(ctx: Context) {\n const products = await Product.findBy({ owner: { id: ctx.user.id } });\n return new HttpResponseOK(products);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Cookies"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"User cookie",src:s(66674).A+"",width:"995",height:"41"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Client-side code"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const user = JSON.parse(decodeURIComponent(/* cookie value */));\n"})}),"\n",(0,t.jsx)(n.h2,{id:"save-and-read-content",children:"Save and Read Content"}),"\n",(0,t.jsxs)(n.p,{children:["You can access and modify the session content with the ",(0,t.jsx)(n.code,{children:"set"})," and ",(0,t.jsx)(n.code,{children:"get"})," methods."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseNoContent, Post, UseSessions } from '@foal/core';\n\n@UseSessions(/* ... */)\nexport class ApiController {\n\n @Post('/subscribe')\n subscribe(ctx: Context) {\n const plan = ctx.session!.get('plan', 'free');\n // ...\n }\n\n @Post('/choose-premium-plan')\n choosePremimumPlan(ctx: Context) {\n ctx.session!.set('plan', 'premium');\n return new HttpResponseNoContent();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"flash-content",children:"Flash Content"}),"\n",(0,t.jsx)(n.p,{children:"Sometimes we may wish to store items in the session only for the next request."}),"\n",(0,t.jsx)(n.p,{children:'For example, when users enter incorrect credentials, they are redirected to the login page, and this time we may want to render the page with a specific message that says "Incorrect email or password". If the user refreshes the page, the message then disappears.'}),"\n",(0,t.jsx)(n.p,{children:"This can be done with flash content. The data will only be available on the next request."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"ctx.session.set('error', 'Incorrect email or password', { flash: true });\n"})}),"\n",(0,t.jsx)(n.h2,{id:"security",children:"Security"}),"\n",(0,t.jsx)(n.h3,{id:"session-expiration-timeouts",children:"Session Expiration Timeouts"}),"\n",(0,t.jsx)(n.p,{children:"Session states has two expiration timeouts."}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Timeout"}),(0,t.jsx)(n.th,{children:"Description"}),(0,t.jsx)(n.th,{children:"Default value"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Inactivity (or idle) timeout"}),(0,t.jsx)(n.td,{children:"Period of inactivity after which the session expires."}),(0,t.jsx)(n.td,{children:"15 minutes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Absolute timeout"}),(0,t.jsx)(n.td,{children:"Period after which the session expires, regardless of its activity."}),(0,t.jsx)(n.td,{children:"1 week"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"If needed, the default values can be override in the configuration. The timeouts must be provided in seconds."}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n expirationTimeouts:\n absolute: 2592000 # 30 days\n inactivity: 1800 # 30 min\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "expirationTimeouts": {\n "absolute": 2592000,\n "inactivity": 1800\n }\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n session: {\n expirationTimeouts: {\n absolute: 2592000, // 30 days\n inactivity: 1800 // 30 min\n }\n }\n }\n}\n"})})})]}),"\n",(0,t.jsx)(n.h3,{id:"revoking-sessions",children:"Revoking Sessions"}),"\n",(0,t.jsx)(n.h4,{id:"revoking-one-session",children:"Revoking One Session"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"foal g script revoke-session\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Open ",(0,t.jsx)(n.code,{children:"scripts/revoke-session.ts"})," and update its content."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createService, readSession, Store } from '@foal/core';\n\nimport { dataSource } from '../db';\n\nexport const schema = {\n type: 'object',\n properties: {\n token: { type: 'string' },\n },\n required: [ 'token' ]\n}\n\nexport async function main({ token }: { token: string }) {\n await dataSource.initialize();\n\n const store = createService(Store);\n await store.boot();\n\n const session = await readSession(store, token);\n if (session) {\n await session.destroy();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Build the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run build\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'foal run revoke-session token="lfdkszjanjiznr"\n'})}),"\n",(0,t.jsx)(n.h4,{id:"revoking-all-sessions",children:"Revoking All Sessions"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"foal g script revoke-all-sessions\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Open ",(0,t.jsx)(n.code,{children:"scripts/revoke-all-sessions.ts"})," and update its content."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createService, Store } from '@foal/core';\n\nimport { dataSource } from '../db';\n\nexport async function main() {\n await dataSource.initialize();\n\n const store = createService(Store);\n await store.boot();\n await store.clear();\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Build the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run build\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"foal run revoke-all-sessions\n"})}),"\n",(0,t.jsx)(n.h3,{id:"query-all-sessions-of-a-user",children:"Query All Sessions of a User"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This feature is only available with the TypeORM store."})}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const user = { id: 1 };\nconst ids = await store.getSessionIDsOf(user.id);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"query-all-connected-users",children:"Query All Connected Users"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This feature is only available with the TypeORM store."})}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const ids = await store.getAuthenticatedUserIds();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"force-the-disconnection-of-a-user",children:"Force the Disconnection of a User"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This feature is only available with the TypeORM store."})}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const user = { id: 1 };\nawait store.destroyAllSessionsOf(user.id);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"re-generate-the-session-id",children:"Re-generate the Session ID"}),"\n",(0,t.jsxs)(n.p,{children:["When a user logs in or change their password, it is a ",(0,t.jsx)(n.a,{href:"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change",children:"good practice"})," to regenerate the session ID. This can be done with the ",(0,t.jsx)(n.code,{children:"regenerateID"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"await ctx.session.regenerateID();\n"})}),"\n",(0,t.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,t.jsx)(n.h3,{id:"specify-the-store-locally",children:"Specify the Store Locally"}),"\n",(0,t.jsxs)(n.p,{children:["By default, the ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook and the ",(0,t.jsx)(n.code,{children:"Store"})," service retrieve the store to use from the configuration. This behavior can be override by importing the store directly into the code."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, UseSessions } from '@foal/core';\nimport { RedisStore } from '@foal/redis';\n\n@UseSessions({ store: RedisStore })\nexport class ApiController {\n\n @dependency\n store: RedisStore;\n\n @Post('/login')\n async login(ctx: Context) {\n // Check the user credentials...\n\n ctx.session = await createSession(this.store);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"cleanup-expired-sessions",children:"Cleanup Expired Sessions"}),"\n",(0,t.jsxs)(n.p,{children:["By default, FoalTS removes expired sessions in ",(0,t.jsx)(n.code,{children:"TypeORMStore"})," and ",(0,t.jsx)(n.code,{children:"MongoDBStore"})," every 50 requests on average. This can be changed with this configuration key:"]}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n garbageCollector:\n periodicity: 25\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "garbageCollector": {\n "periodicity": 25\n }\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n session: {\n garbageCollector: {\n periodicity: 25\n }\n }\n }\n}\n"})})})]}),"\n",(0,t.jsx)(n.h3,{id:"implement-a-custom-store",children:"Implement a Custom Store"}),"\n",(0,t.jsxs)(n.p,{children:["If necessary, you can implement your own session store. This one must inherit the abstract class ",(0,t.jsx)(n.code,{children:"SessionStore"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["To use it, your can import it directly in your code (see the section ",(0,t.jsx)(n.em,{children:"Specify the Store Locally"}),") or use a ",(0,t.jsx)(n.a,{href:"/docs/architecture/services-and-dependency-injection#abstract-services",children:"relative path"})," in the configuration. In this case, the class must be exported with the name ",(0,t.jsx)(n.code,{children:"ConcreteSessionStore"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SessionState, SessionStore } from '@foal/core';\n\nclass CustomSessionStore extends SessionStore {\n save(state: SessionState, maxInactivity: number): Promise {\n // ...\n }\n read(id: string): Promise {\n // ...\n }\n update(state: SessionState, maxInactivity: number): Promise {\n // ...\n }\n destroy(id: string): Promise {\n // ...\n }\n clear(): Promise {\n // ...\n }\n cleanUpExpiredSessions(maxInactivity: number, maxLifeTime: number): Promise {\n // ...\n }\n}\n"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Method"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"save"})}),(0,t.jsx)(n.td,{children:"Saves the session for the first time. If a session already exists with the given ID, a SessionAlreadyExists error MUST be thrown."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"read"})}),(0,t.jsxs)(n.td,{children:["Reads a session. If the session does not exist, the value ",(0,t.jsx)(n.code,{children:"null"})," MUST be returned."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"update"})}),(0,t.jsx)(n.td,{children:"Updates and extends the lifetime of a session. If the session no longer exists (i.e. has expired or been destroyed), the session MUST still be saved."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"destroy"})}),(0,t.jsx)(n.td,{children:"Deletes a session. If the session does not exist, NO error MUST be thrown."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"clear"})}),(0,t.jsx)(n.td,{children:"Clears all sessions."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"cleanUpExpiredSessions"})}),(0,t.jsx)(n.td,{children:"Some session stores may need to run periodically background jobs to cleanup expired sessions. This method deletes all expired sessions. If the store manages a cache database, then this method can remain empty but it must NOT throw an error."})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Session stores do not manipulate ",(0,t.jsx)(n.code,{children:"Session"})," instances directly. Instead, they use ",(0,t.jsx)(n.code,{children:"SessionState"})," objects."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"interface SessionState {\n // 44-characters long\n id: string;\n userId: string|number|null;\n content: { [key: string]: any };\n flash: { [key: string]: any };\n // 4-bytes long (min: 0, max: 2147483647)\n updatedAt: number;\n // 4-bytes long (min: 0, max: 2147483647)\n createdAt: number;\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"usage-with-cookies-1",children:"Usage with Cookies"}),"\n",(0,t.jsx)(n.h4,{id:"do-not-auto-create-the-session",children:"Do not Auto-Create the Session"}),"\n",(0,t.jsxs)(n.p,{children:["By default, when the ",(0,t.jsx)(n.code,{children:"cookie"})," option is set to true, the ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook automatically creates a session if it does not already exist. This can be disabled with the ",(0,t.jsx)(n.code,{children:"create"})," option."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\nexport class ApiController {\n @dependency\n store: Store;\n\n @Post('/login')\n @UseSessions({ cookie: true, create: false })\n async login(ctx: Context) {\n // Check the credentials...\n\n // ctx.session is potentially undefined\n if (!ctx.session) {\n ctx.session = await createSession(this.store);\n }\n\n return new HttpResponseOK();\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"override-the-cookie-options",children:"Override the Cookie Options"}),"\n",(0,t.jsx)(n.p,{children:"The default session cookie directives can be overridden in the configuration as follows:"}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n cookie:\n name: xxx # default: sessionID\n domain: example.com\n httpOnly: false # default: true\n path: /foo # default: /\n sameSite: lax\n secure: true\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "cookie": {\n "name": "xxx",\n "domain": "example.com",\n "httpOnly": false,\n "path": "/foo",\n "sameSite": "lax",\n "secure": true\n }\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n cookie: {\n name: "xxx", // default: sessionID\n domain: "example.com",\n httpOnly: false, // default: true\n path: "/foo", // default: /\n sameSite: "lax",\n secure: true\n }\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.h4,{id:"require-the-cookie",children:"Require the Cookie"}),"\n",(0,t.jsxs)(n.p,{children:["In rare situations, you may want to return a 400 error or redirect the user if no session cookie already exists on the client. If so, you can use the ",(0,t.jsx)(n.code,{children:"required"})," option to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UseSessions } from '@foal/core';\n\nexport class ApiController {\n\n @Get('/products')\n @UseSessions({ cookie: true, required: true })\n readProducts() {\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"read-a-session-from-a-token",children:"Read a Session From a Token"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook automatically retrieves the session state on each request. If you need to manually read a session (for example in a shell script), you can do it with the ",(0,t.jsx)(n.code,{children:"readSession"})," function."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { readSession } from '@foal/core';\n\nconst session = await readSession(store, token);\nif (!session) {\n throw new Error('Session does not exist or has expired.')\n}\nconst foo = session.get('foo');\n"})}),"\n",(0,t.jsx)(n.h3,{id:"save-manually-a-session",children:"Save Manually a Session"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook automatically saves the session state on each request. If you need to manually save a session (for example in a shell script), you can do it with the ",(0,t.jsx)(n.code,{children:"commit"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"await session.commit();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"provide-a-custom-client-to-use-in-the-stores",children:"Provide A Custom Client to Use in the Stores"}),"\n",(0,t.jsxs)(n.p,{children:["By default, the ",(0,t.jsx)(n.code,{children:"MongoDBStore"})," and ",(0,t.jsx)(n.code,{children:"RedisStore"})," create a new client to connect to their respective databases."]}),"\n",(0,t.jsx)(n.p,{children:"This behavior can be overridden by providing a custom client to the stores at initialization."}),"\n",(0,t.jsx)(n.h4,{id:"redisstore-1",children:(0,t.jsx)(n.code,{children:"RedisStore"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install redis@4\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"index.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { RedisStore } from '@foal/redis';\nimport { createClient } from 'redis';\n\nasync function main() {\n const redisClient = createClient({ url: 'redis://localhost:6379' });\n await redisClient.connect();\n\n\n const serviceManager = new ServiceManager();\n serviceManager.get(RedisStore).setRedisClient(redisClient);\n\n const app = await createApp(AppController, { serviceManager });\n\n // ...\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"mongodbstore-1",children:(0,t.jsx)(n.code,{children:"MongoDBStore"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install mongodb@5\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"index.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { MongoDBStore } from '@foal/mongodb';\nimport { MongoClient } from 'mongodb';\n\nasync function main() {\n const mongoDBClient = await MongoClient.connect('mongodb://localhost:27017/db');\n\n const serviceManager = new ServiceManager();\n serviceManager.get(MongoDBStore).setMongoDBClient(mongoDBClient);\n\n const app = await createApp(AppController, { serviceManager });\n\n // ...\n}\n"})})]})}function p(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},19365:(e,n,s)=>{s.d(n,{A:()=>r});s(96540);var t=s(34164);const o={tabItem:"tabItem_Ymn6"};var i=s(74848);function r(e){let{children:n,hidden:s,className:r}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,t.A)(o.tabItem,r),hidden:s,children:n})}},11470:(e,n,s)=>{s.d(n,{A:()=>S});var t=s(96540),o=s(34164),i=s(23104),r=s(56347),a=s(205),c=s(57485),l=s(31682),d=s(89466);function h(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:s}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:s,attributes:t,default:o}}=e;return{value:n,label:s,attributes:t,default:o}}))}(s);return function(e){const n=(0,l.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,s])}function p(e){let{value:n,tabValues:s}=e;return s.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:s}=e;const o=(0,r.W6)(),i=function(e){let{queryString:n=!1,groupId:s}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!s)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return s??null}({queryString:n,groupId:s});return[(0,c.aZ)(i),(0,t.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(o.location.search);n.set(i,e),o.replace({...o.location,search:n.toString()})}),[i,o])]}function m(e){const{defaultValue:n,queryString:s=!1,groupId:o}=e,i=u(e),[r,c]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:s}=e;if(0===s.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:s}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${s.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=s.find((e=>e.default))??s[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:i}))),[l,h]=x({queryString:s,groupId:o}),[m,j]=function(e){let{groupId:n}=e;const s=function(e){return e?`docusaurus.tab.${e}`:null}(n),[o,i]=(0,d.Dv)(s);return[o,(0,t.useCallback)((e=>{s&&i.set(e)}),[s,i])]}({groupId:o}),g=(()=>{const e=l??m;return p({value:e,tabValues:i})?e:null})();(0,a.A)((()=>{g&&c(g)}),[g]);return{selectedValue:r,selectValue:(0,t.useCallback)((e=>{if(!p({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);c(e),h(e),j(e)}),[h,j,i]),tabValues:i}}var j=s(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var f=s(74848);function y(e){let{className:n,block:s,selectedValue:t,selectValue:r,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,i.a_)(),d=e=>{const n=e.currentTarget,s=c.indexOf(n),o=a[s].value;o!==t&&(l(n),r(o))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const s=c.indexOf(e.currentTarget)+1;n=c[s]??c[0];break}case"ArrowLeft":{const s=c.indexOf(e.currentTarget)-1;n=c[s]??c[c.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":s},n),children:a.map((e=>{let{value:n,label:s,attributes:i}=e;return(0,f.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>c.push(e),onKeyDown:h,onClick:d,...i,className:(0,o.A)("tabs__item",g.tabItem,i?.className,{"tabs__item--active":t===n}),children:s??n},n)}))})}function v(e){let{lazy:n,children:s,selectedValue:o}=e;const i=(Array.isArray(s)?s:[s]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===o));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==o})))})}function b(e){const n=m(e);return(0,f.jsxs)("div",{className:(0,o.A)("tabs-container",g.tabList),children:[(0,f.jsx)(y,{...e,...n}),(0,f.jsx)(v,{...e,...n})]})}function S(e){const n=(0,j.A)();return(0,f.jsx)(b,{...e,children:h(e.children)},String(n))}},66674:(e,n,s)=>{s.d(n,{A:()=>t});const t=s.p+"assets/images/user-cookie-c3f0e06d9ced0800240ac2b5ee368b8c.png"},28453:(e,n,s)=>{s.d(n,{R:()=>r,x:()=>a});var t=s(96540);const o={},i=t.createContext(o);function r(e){const n=t.useContext(i);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[6491],{55066:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>a,metadata:()=>l,toc:()=>h});var t=s(74848),o=s(28453),i=s(11470),r=s(19365);const a={title:"Session Tokens",sidebar_label:"Session Tokens"},c=void 0,l={id:"authentication/session-tokens",title:"Session Tokens",description:"Introduction",source:"@site/docs/authentication/session-tokens.md",sourceDirName:"authentication",slug:"/authentication/session-tokens",permalink:"/docs/authentication/session-tokens",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/session-tokens.md",tags:[],version:"current",frontMatter:{title:"Session Tokens",sidebar_label:"Session Tokens"},sidebar:"someSidebar",previous:{title:"Passwords",permalink:"/docs/authentication/password-management"},next:{title:"JSON Web Tokens",permalink:"/docs/authentication/jwt"}},d={},h=[{value:"Introduction",id:"introduction",level:2},{value:"The Basics",id:"the-basics",level:2},{value:"Choosing a session store",id:"choosing-a-session-store",level:3},{value:"TypeORMStore",id:"typeormstore",level:4},{value:"RedisStore",id:"redisstore",level:4},{value:"MongoDBStore",id:"mongodbstore",level:4},{value:"Usage with the Authorization header",id:"usage-with-the-authorization-header",level:3},{value:"Usage with cookies",id:"usage-with-cookies",level:3},{value:"Adding authentication and access control",id:"adding-authentication-and-access-control",level:3},{value:"Destroying the session",id:"destroying-the-session",level:3},{value:"Reading User Information on the Client (cookies)",id:"reading-user-information-on-the-client-cookies",level:2},{value:"Save and Read Content",id:"save-and-read-content",level:2},{value:"Flash Content",id:"flash-content",level:3},{value:"Security",id:"security",level:2},{value:"Session Expiration Timeouts",id:"session-expiration-timeouts",level:3},{value:"Revoking Sessions",id:"revoking-sessions",level:3},{value:"Revoking One Session",id:"revoking-one-session",level:4},{value:"Revoking All Sessions",id:"revoking-all-sessions",level:4},{value:"Query All Sessions of a User",id:"query-all-sessions-of-a-user",level:3},{value:"Query All Connected Users",id:"query-all-connected-users",level:3},{value:"Force the Disconnection of a User",id:"force-the-disconnection-of-a-user",level:3},{value:"Re-generate the Session ID",id:"re-generate-the-session-id",level:3},{value:"Advanced",id:"advanced",level:2},{value:"Specify the Store Locally",id:"specify-the-store-locally",level:3},{value:"Cleanup Expired Sessions",id:"cleanup-expired-sessions",level:3},{value:"Implement a Custom Store",id:"implement-a-custom-store",level:3},{value:"Usage with Cookies",id:"usage-with-cookies-1",level:3},{value:"Do not Auto-Create the Session",id:"do-not-auto-create-the-session",level:4},{value:"Override the Cookie Options",id:"override-the-cookie-options",level:4},{value:"Require the Cookie",id:"require-the-cookie",level:4},{value:"Read a Session From a Token",id:"read-a-session-from-a-token",level:3},{value:"Save Manually a Session",id:"save-manually-a-session",level:3},{value:"Provide A Custom Client to Use in the Stores",id:"provide-a-custom-client-to-use-in-the-stores",level:3},{value:"RedisStore",id:"redisstore-1",level:4},{value:"MongoDBStore",id:"mongodbstore-1",level:4}];function u(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h2,{id:"introduction",children:"Introduction"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This document assumes that you have alread read the ",(0,t.jsx)(n.a,{href:"/docs/authentication/quick-start",children:"Quick Start"})," page."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In FoalTS, web sessions are temporary states that can be associated with a specific user. They are identified by a token and are mainly used to keep users authenticated between several HTTP requests (the client sends the token on each request to authenticate the user)."}),"\n",(0,t.jsx)(n.p,{children:"A session usually begins when the user logs in (or starts visiting the website) and ends after a period of inactivity or when the user logs out. By inactivity, we mean that the server no longer receives requests from the authenticated user for a certain period of time."}),"\n",(0,t.jsx)(n.h2,{id:"the-basics",children:"The Basics"}),"\n",(0,t.jsx)(n.h3,{id:"choosing-a-session-store",children:"Choosing a session store"}),"\n",(0,t.jsxs)(n.p,{children:["To begin, you must first specify where the session states will be stored. FoalTS provides several ",(0,t.jsx)(n.em,{children:"session stores"})," for this. For example, you can use the ",(0,t.jsx)(n.code,{children:"TypeORMStore"})," to save the sessions in your SQL database or the ",(0,t.jsx)(n.code,{children:"RedisStore"})," to save them in a redis cache."]}),"\n",(0,t.jsxs)(n.p,{children:["To do so, the package name of the store must be provided with the configuration key ",(0,t.jsx)(n.code,{children:"settings.session.store"}),"."]}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:'settings:\n session:\n store: "@foal/typeorm"\n'})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/typeorm",\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/typeorm",\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.h4,{id:"typeormstore",children:"TypeORMStore"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install typeorm@0.3.17 @foal/typeorm\n"})}),"\n",(0,t.jsxs)(n.p,{children:["This store uses the default TypeORM connection whose configuration is usually specified in ",(0,t.jsx)(n.code,{children:"config/default.{json|yml|js}"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Session states are saved in the ",(0,t.jsx)(n.code,{children:"databasesession"})," table of your SQL database. In order to create it, you need to add and run migrations. For this purpose, you can export the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity in your user file and execute the following commands."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"entities/user.entity.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { DatabaseSession } from '@foal/typeorm';\nimport { BaseEntity, Entity } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n /* ... */\n}\n\nexport { DatabaseSession }\n"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Warning"}),": If you use TypeORM store, then your entity IDs must be numbers (not strings)."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"redisstore",children:"RedisStore"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install @foal/redis\n"})}),"\n",(0,t.jsx)(n.p,{children:"In order to use this store, you must provide the redis URI in the configuration."}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n store: \"@foal/redis\"\n redis:\n uri: 'redis://localhost:6379'\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/redis",\n },\n "redis": {\n "uri": "redis://localhost:6379"\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/redis",\n },\n redis: {\n uri: "redis://localhost:6379"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If you are using the redis store, please ensure that you have a ",(0,t.jsx)(n.code,{children:"@dependency store: Store"})," property in at least one of your controllers or services. Otherwise, the connection to the redis database will not be established when the application starts."]}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"mongodbstore",children:"MongoDBStore"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install @foal/mongodb\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This package uses the ",(0,t.jsx)(n.a,{href:"https://www.npmjs.com/package/mongodb",children:"mongodb Node.JS driver"})," which uses the ",(0,t.jsx)(n.a,{href:"https://www.npmjs.com/package/@types/node",children:"@types/node"})," package under the hood. If you get type compilation errors, try to upgrade this dependency."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["This store saves your session states in a MongoDB database (using the collection ",(0,t.jsx)(n.code,{children:"sessions"}),"). In order to use it, you must provide the MongoDB URI in the configuration."]}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n store: \"@foal/mongodb\"\n mongodb:\n uri: 'mongodb://localhost:27017'\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "store": "@foal/mongodb",\n },\n "mongodb": {\n "uri": "mongodb://localhost:27017"\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n store: "@foal/mongodb",\n },\n mongodb: {\n uri: "mongodb://localhost:27017"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If you are using the MongoDB store, please ensure that you have a ",(0,t.jsx)(n.code,{children:"@dependency store: Store"})," property in at least one of your controllers or services. Otherwise, the connection to the MondoDB database will not be established when the application starts."]}),"\n"]}),"\n",(0,t.jsxs)(n.h3,{id:"usage-with-the-authorization-header",children:["Usage with the ",(0,t.jsx)(n.code,{children:"Authorization"})," header"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This section explains how to use sessions with a ",(0,t.jsx)(n.code,{children:"bearer"})," token and the ",(0,t.jsx)(n.code,{children:"Authorization"})," header. See the section below to see how to use them with cookies."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"The mechanism is as follows:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Upon login, create the session and assign it to ",(0,t.jsx)(n.code,{children:"ctx.session"}),". Then return the session token in the response."]}),"\n",(0,t.jsxs)(n.li,{children:["On subsequent requests, send the token in the ",(0,t.jsx)(n.code,{children:"Authorization"})," header with this scheme: ",(0,t.jsx)(n.code,{children:"Authorization: Bearer "}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\n@UseSessions()\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n async login(ctx: Context) {\n // Check the user credentials...\n\n ctx.session = await createSession(this.store);\n\n // See the \"authentication\" section below\n // to see how to associate a user to the session.\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n // If the request has an Authorization header with a valid token\n // then ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["If the ",(0,t.jsx)(n.code,{children:"Authorization"})," header does not use the ",(0,t.jsx)(n.code,{children:"bearer"})," scheme or if the token is invalid or expired, then the hook returns a 400 or 401 error."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["If you want to make sure that ",(0,t.jsx)(n.code,{children:"ctx.session"})," is set and get a 400 error if no ",(0,t.jsx)(n.code,{children:"Authorization"})," header is provided, you can use the ",(0,t.jsx)(n.code,{children:"required"})," option for this."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n @UseSessions()\n async login(ctx: Context) {\n // Check the user credentials...\n\n ctx.session = await createSession(this.store);\n\n // See the \"authentication\" section below\n // to see how to associate a user to the session.\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n @UseSessions({ required: true })\n readProducts(ctx: Context) {\n // ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"usage-with-cookies",children:"Usage with cookies"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This section explains how to use sessions with cookies. See the section above to see how to use them with a ",(0,t.jsx)(n.code,{children:"bearer"})," token and the ",(0,t.jsx)(n.code,{children:"Authorization"})," header."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["Be aware that if you use cookies, your application must provide a ",(0,t.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF defense"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["When using the ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook with the ",(0,t.jsx)(n.code,{children:"cookie"})," option, FoalTS makes sure that ",(0,t.jsx)(n.code,{children:"ctx.session"})," is always set and takes care of managing the session token on the client (using a cookie)."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\n@UseSessions({ cookie: true })\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n login(ctx: Context) {\n // Check the user credentials...\n\n // See the \"authentication\" section below\n // to see how to associate a user to the session.\n\n return new HttpResponseOK();\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n // ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If the session has expired, the hook returns a 401 error. If you want to redirect the user to the login page, you can use the ",(0,t.jsx)(n.code,{children:"redirectTo"})," option to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"@UseSessions({\n cookie: true,\n redirectTo: '/login'\n})\nexport class ApiController {\n\n @Get('/products')\n readProducts(ctx: Context) {\n // ctx.session is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"adding-authentication-and-access-control",children:"Adding authentication and access control"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["This section explains how to associate a specific user to a session and how to use ",(0,t.jsx)(n.code,{children:"ctx.user"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Sessions can be used to authenticate users. To do this, you can use the ",(0,t.jsx)(n.code,{children:"Session.setUser"})," method and the ",(0,t.jsx)(n.code,{children:"user"})," option of ",(0,t.jsx)(n.code,{children:"@UseSessions"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n@UseSessions({\n // If the session is attached to a user,\n // then retrieve the user from the database\n // and assign it to ctx.user\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n @dependency\n store: Store;\n\n @Post('/login')\n async login(ctx: Context) {\n // Check the user credentials...\n // const user = ...\n\n ctx.session = await createSession(this.store);\n\n // Attach the user to the session.\n ctx.session.setUser(user);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n // If the ctx.session is defined and the session is attached to a user\n // then ctx.user is an instance of User. Otherwise it is null.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If you want to restrict certain routes to authenticated users, you can use the ",(0,t.jsx)(n.code,{children:"@UserRequired"})," hook for this."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK, UserRequired, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n@UseSessions({\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n @Get('/products')\n @UserRequired()\n readProducts(ctx: Context) {\n // ctx.user is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If the user is not authenticated, the hook returns a 401 error. If you want to redirect the user to the login page, you can use the ",(0,t.jsx)(n.code,{children:"redirectTo"})," option to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, Get, HttpResponseOK, UserRequired, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n@UseSessions({\n redirectTo: '/login',\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class ApiController {\n\n @Get('/products')\n @UserRequired({\n redirectTo: '/login'\n })\n readProducts(ctx: Context) {\n // ctx.user is defined.\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"destroying-the-session",children:"Destroying the session"}),"\n",(0,t.jsxs)(n.p,{children:["Sessions can be destroyed (i.e users can be logged out) using their ",(0,t.jsx)(n.code,{children:"destroy"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseNoContent, Post, UseSessions } from '@foal/core';\n\nexport class AuthController {\n\n @Post('/logout')\n @UseSessions()\n async logout(ctx: Context) {\n if (ctx.session) {\n await ctx.session.destroy();\n }\n return new HttpResponseNoContent();\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"reading-user-information-on-the-client-cookies",children:"Reading User Information on the Client (cookies)"}),"\n",(0,t.jsxs)(n.p,{children:["When building a SPA with cookie-based authentication, it can sometimes be difficult to know if the user is logged in or to obtain certain information about the user (",(0,t.jsx)(n.code,{children:"isAdmin"}),", etc)."]}),"\n",(0,t.jsxs)(n.p,{children:["Since the authentication token is stored in a cookie with the ",(0,t.jsx)(n.code,{children:"httpOnly"})," directive set to ",(0,t.jsx)(n.code,{children:"true"})," (to mitigate XSS attacks), the front-end application has no way of knowing if a user is logged in, except by making an additional request to the server."]}),"\n",(0,t.jsxs)(n.p,{children:["To solve this problem, Foal provides an option called ",(0,t.jsx)(n.code,{children:"userCookie"})," that allows you to set an additional cookie that the frontend can read with the content you choose. This cookie is synchronized with the session and is refreshed at each request and destroyed when the session expires or when the user logs out."]}),"\n",(0,t.jsxs)(n.p,{children:["In the following example, the ",(0,t.jsx)(n.code,{children:"user"})," cookie is empty if no user is logged in or contains certain information about him/her otherwise. This is particularly useful if you need to display UI elements based on user characteristics."]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Server-side code"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"function userToJSON(user: User|null) {\n if (!user) {\n return 'null';\n }\n\n return JSON.stringify({\n email: user.email,\n isAdmin: user.isAdmin\n });\n}\n\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n userCookie: (ctx, services) => userToJSON(ctx.user as User|null)\n})\nexport class ApiController {\n\n @Get('/products')\n @UserRequired()\n async readProducts(ctx: Context) {\n const products = await Product.findBy({ owner: { id: ctx.user.id } });\n return new HttpResponseOK(products);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Cookies"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"User cookie",src:s(66674).A+"",width:"995",height:"41"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"Client-side code"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"const user = JSON.parse(decodeURIComponent(/* cookie value */));\n"})}),"\n",(0,t.jsx)(n.h2,{id:"save-and-read-content",children:"Save and Read Content"}),"\n",(0,t.jsxs)(n.p,{children:["You can access and modify the session content with the ",(0,t.jsx)(n.code,{children:"set"})," and ",(0,t.jsx)(n.code,{children:"get"})," methods."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, HttpResponseNoContent, Post, UseSessions } from '@foal/core';\n\n@UseSessions(/* ... */)\nexport class ApiController {\n\n @Post('/subscribe')\n subscribe(ctx: Context) {\n const plan = ctx.session!.get('plan', 'free');\n // ...\n }\n\n @Post('/choose-premium-plan')\n choosePremimumPlan(ctx: Context) {\n ctx.session!.set('plan', 'premium');\n return new HttpResponseNoContent();\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"flash-content",children:"Flash Content"}),"\n",(0,t.jsx)(n.p,{children:"Sometimes we may wish to store items in the session only for the next request."}),"\n",(0,t.jsx)(n.p,{children:'For example, when users enter incorrect credentials, they are redirected to the login page, and this time we may want to render the page with a specific message that says "Incorrect email or password". If the user refreshes the page, the message then disappears.'}),"\n",(0,t.jsx)(n.p,{children:"This can be done with flash content. The data will only be available on the next request."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"ctx.session.set('error', 'Incorrect email or password', { flash: true });\n"})}),"\n",(0,t.jsx)(n.h2,{id:"security",children:"Security"}),"\n",(0,t.jsx)(n.h3,{id:"session-expiration-timeouts",children:"Session Expiration Timeouts"}),"\n",(0,t.jsx)(n.p,{children:"Session states has two expiration timeouts."}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Timeout"}),(0,t.jsx)(n.th,{children:"Description"}),(0,t.jsx)(n.th,{children:"Default value"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Inactivity (or idle) timeout"}),(0,t.jsx)(n.td,{children:"Period of inactivity after which the session expires."}),(0,t.jsx)(n.td,{children:"15 minutes"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Absolute timeout"}),(0,t.jsx)(n.td,{children:"Period after which the session expires, regardless of its activity."}),(0,t.jsx)(n.td,{children:"1 week"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"If needed, the default values can be override in the configuration. The timeouts must be provided in seconds."}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n expirationTimeouts:\n absolute: 2592000 # 30 days\n inactivity: 1800 # 30 min\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "expirationTimeouts": {\n "absolute": 2592000,\n "inactivity": 1800\n }\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n session: {\n expirationTimeouts: {\n absolute: 2592000, // 30 days\n inactivity: 1800 // 30 min\n }\n }\n }\n}\n"})})})]}),"\n",(0,t.jsx)(n.h3,{id:"revoking-sessions",children:"Revoking Sessions"}),"\n",(0,t.jsx)(n.h4,{id:"revoking-one-session",children:"Revoking One Session"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npx foal g script revoke-session\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Open ",(0,t.jsx)(n.code,{children:"scripts/revoke-session.ts"})," and update its content."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createService, readSession, Store } from '@foal/core';\n\nimport { dataSource } from '../db';\n\nexport const schema = {\n type: 'object',\n properties: {\n token: { type: 'string' },\n },\n required: [ 'token' ]\n}\n\nexport async function main({ token }: { token: string }) {\n await dataSource.initialize();\n\n const store = createService(Store);\n await store.boot();\n\n const session = await readSession(store, token);\n if (session) {\n await session.destroy();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Build the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run build\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:'npx foal run revoke-session token="lfdkszjanjiznr"\n'})}),"\n",(0,t.jsx)(n.h4,{id:"revoking-all-sessions",children:"Revoking All Sessions"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npx foal g script revoke-all-sessions\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Open ",(0,t.jsx)(n.code,{children:"scripts/revoke-all-sessions.ts"})," and update its content."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createService, Store } from '@foal/core';\n\nimport { dataSource } from '../db';\n\nexport async function main() {\n await dataSource.initialize();\n\n const store = createService(Store);\n await store.boot();\n await store.clear();\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Build the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm run build\n"})}),"\n",(0,t.jsx)(n.p,{children:"Run the script."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npx foal run revoke-all-sessions\n"})}),"\n",(0,t.jsx)(n.h3,{id:"query-all-sessions-of-a-user",children:"Query All Sessions of a User"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This feature is only available with the TypeORM store."})}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const user = { id: 1 };\nconst ids = await store.getSessionIDsOf(user.id);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"query-all-connected-users",children:"Query All Connected Users"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This feature is only available with the TypeORM store."})}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const ids = await store.getAuthenticatedUserIds();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"force-the-disconnection-of-a-user",children:"Force the Disconnection of a User"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"This feature is only available with the TypeORM store."})}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"const user = { id: 1 };\nawait store.destroyAllSessionsOf(user.id);\n"})}),"\n",(0,t.jsx)(n.h3,{id:"re-generate-the-session-id",children:"Re-generate the Session ID"}),"\n",(0,t.jsxs)(n.p,{children:["When a user logs in or change their password, it is a ",(0,t.jsx)(n.a,{href:"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change",children:"good practice"})," to regenerate the session ID. This can be done with the ",(0,t.jsx)(n.code,{children:"regenerateID"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"await ctx.session.regenerateID();\n"})}),"\n",(0,t.jsx)(n.h2,{id:"advanced",children:"Advanced"}),"\n",(0,t.jsx)(n.h3,{id:"specify-the-store-locally",children:"Specify the Store Locally"}),"\n",(0,t.jsxs)(n.p,{children:["By default, the ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook and the ",(0,t.jsx)(n.code,{children:"Store"})," service retrieve the store to use from the configuration. This behavior can be override by importing the store directly into the code."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, Get, HttpResponseOK, Post, UseSessions } from '@foal/core';\nimport { RedisStore } from '@foal/redis';\n\n@UseSessions({ store: RedisStore })\nexport class ApiController {\n\n @dependency\n store: RedisStore;\n\n @Post('/login')\n async login(ctx: Context) {\n // Check the user credentials...\n\n ctx.session = await createSession(this.store);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Get('/products')\n readProducts(ctx: Context) {\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"cleanup-expired-sessions",children:"Cleanup Expired Sessions"}),"\n",(0,t.jsxs)(n.p,{children:["By default, FoalTS removes expired sessions in ",(0,t.jsx)(n.code,{children:"TypeORMStore"})," and ",(0,t.jsx)(n.code,{children:"MongoDBStore"})," every 50 requests on average. This can be changed with this configuration key:"]}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n garbageCollector:\n periodicity: 25\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "garbageCollector": {\n "periodicity": 25\n }\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:"module.exports = {\n settings: {\n session: {\n garbageCollector: {\n periodicity: 25\n }\n }\n }\n}\n"})})})]}),"\n",(0,t.jsx)(n.h3,{id:"implement-a-custom-store",children:"Implement a Custom Store"}),"\n",(0,t.jsxs)(n.p,{children:["If necessary, you can implement your own session store. This one must inherit the abstract class ",(0,t.jsx)(n.code,{children:"SessionStore"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["To use it, your can import it directly in your code (see the section ",(0,t.jsx)(n.em,{children:"Specify the Store Locally"}),") or use a ",(0,t.jsx)(n.a,{href:"/docs/architecture/services-and-dependency-injection#abstract-services",children:"relative path"})," in the configuration. In this case, the class must be exported with the name ",(0,t.jsx)(n.code,{children:"ConcreteSessionStore"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { SessionState, SessionStore } from '@foal/core';\n\nclass CustomSessionStore extends SessionStore {\n save(state: SessionState, maxInactivity: number): Promise {\n // ...\n }\n read(id: string): Promise {\n // ...\n }\n update(state: SessionState, maxInactivity: number): Promise {\n // ...\n }\n destroy(id: string): Promise {\n // ...\n }\n clear(): Promise {\n // ...\n }\n cleanUpExpiredSessions(maxInactivity: number, maxLifeTime: number): Promise {\n // ...\n }\n}\n"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Method"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"save"})}),(0,t.jsx)(n.td,{children:"Saves the session for the first time. If a session already exists with the given ID, a SessionAlreadyExists error MUST be thrown."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"read"})}),(0,t.jsxs)(n.td,{children:["Reads a session. If the session does not exist, the value ",(0,t.jsx)(n.code,{children:"null"})," MUST be returned."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"update"})}),(0,t.jsx)(n.td,{children:"Updates and extends the lifetime of a session. If the session no longer exists (i.e. has expired or been destroyed), the session MUST still be saved."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"destroy"})}),(0,t.jsx)(n.td,{children:"Deletes a session. If the session does not exist, NO error MUST be thrown."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"clear"})}),(0,t.jsx)(n.td,{children:"Clears all sessions."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"cleanUpExpiredSessions"})}),(0,t.jsx)(n.td,{children:"Some session stores may need to run periodically background jobs to cleanup expired sessions. This method deletes all expired sessions. If the store manages a cache database, then this method can remain empty but it must NOT throw an error."})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Session stores do not manipulate ",(0,t.jsx)(n.code,{children:"Session"})," instances directly. Instead, they use ",(0,t.jsx)(n.code,{children:"SessionState"})," objects."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"interface SessionState {\n // 44-characters long\n id: string;\n userId: string|number|null;\n content: { [key: string]: any };\n flash: { [key: string]: any };\n // 4-bytes long (min: 0, max: 2147483647)\n updatedAt: number;\n // 4-bytes long (min: 0, max: 2147483647)\n createdAt: number;\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"usage-with-cookies-1",children:"Usage with Cookies"}),"\n",(0,t.jsx)(n.h4,{id:"do-not-auto-create-the-session",children:"Do not Auto-Create the Session"}),"\n",(0,t.jsxs)(n.p,{children:["By default, when the ",(0,t.jsx)(n.code,{children:"cookie"})," option is set to true, the ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook automatically creates a session if it does not already exist. This can be disabled with the ",(0,t.jsx)(n.code,{children:"create"})," option."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, HttpResponseOK, Post, Store, UseSessions } from '@foal/core';\n\nexport class ApiController {\n @dependency\n store: Store;\n\n @Post('/login')\n @UseSessions({ cookie: true, create: false })\n async login(ctx: Context) {\n // Check the credentials...\n\n // ctx.session is potentially undefined\n if (!ctx.session) {\n ctx.session = await createSession(this.store);\n }\n\n return new HttpResponseOK();\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"override-the-cookie-options",children:"Override the Cookie Options"}),"\n",(0,t.jsx)(n.p,{children:"The default session cookie directives can be overridden in the configuration as follows:"}),"\n",(0,t.jsxs)(i.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(r.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n session:\n cookie:\n name: xxx # default: sessionID\n domain: example.com\n httpOnly: false # default: true\n path: /foo # default: /\n sameSite: lax\n secure: true\n"})})}),(0,t.jsx)(r.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "session": {\n "cookie": {\n "name": "xxx",\n "domain": "example.com",\n "httpOnly": false,\n "path": "/foo",\n "sameSite": "lax",\n "secure": true\n }\n }\n }\n}\n'})})}),(0,t.jsx)(r.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'module.exports = {\n settings: {\n session: {\n cookie: {\n name: "xxx", // default: sessionID\n domain: "example.com",\n httpOnly: false, // default: true\n path: "/foo", // default: /\n sameSite: "lax",\n secure: true\n }\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.h4,{id:"require-the-cookie",children:"Require the Cookie"}),"\n",(0,t.jsxs)(n.p,{children:["In rare situations, you may want to return a 400 error or redirect the user if no session cookie already exists on the client. If so, you can use the ",(0,t.jsx)(n.code,{children:"required"})," option to do so."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UseSessions } from '@foal/core';\n\nexport class ApiController {\n\n @Get('/products')\n @UseSessions({ cookie: true, required: true })\n readProducts() {\n return new HttpResponseOK([]);\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"read-a-session-from-a-token",children:"Read a Session From a Token"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook automatically retrieves the session state on each request. If you need to manually read a session (for example in a shell script), you can do it with the ",(0,t.jsx)(n.code,{children:"readSession"})," function."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { readSession } from '@foal/core';\n\nconst session = await readSession(store, token);\nif (!session) {\n throw new Error('Session does not exist or has expired.')\n}\nconst foo = session.get('foo');\n"})}),"\n",(0,t.jsx)(n.h3,{id:"save-manually-a-session",children:"Save Manually a Session"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"@UseSessions"})," hook automatically saves the session state on each request. If you need to manually save a session (for example in a shell script), you can do it with the ",(0,t.jsx)(n.code,{children:"commit"})," method."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"await session.commit();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"provide-a-custom-client-to-use-in-the-stores",children:"Provide A Custom Client to Use in the Stores"}),"\n",(0,t.jsxs)(n.p,{children:["By default, the ",(0,t.jsx)(n.code,{children:"MongoDBStore"})," and ",(0,t.jsx)(n.code,{children:"RedisStore"})," create a new client to connect to their respective databases."]}),"\n",(0,t.jsx)(n.p,{children:"This behavior can be overridden by providing a custom client to the stores at initialization."}),"\n",(0,t.jsx)(n.h4,{id:"redisstore-1",children:(0,t.jsx)(n.code,{children:"RedisStore"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install redis@4\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"index.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { RedisStore } from '@foal/redis';\nimport { createClient } from 'redis';\n\nasync function main() {\n const redisClient = createClient({ url: 'redis://localhost:6379' });\n await redisClient.connect();\n\n\n const serviceManager = new ServiceManager();\n serviceManager.get(RedisStore).setRedisClient(redisClient);\n\n const app = await createApp(AppController, { serviceManager });\n\n // ...\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"mongodbstore-1",children:(0,t.jsx)(n.code,{children:"MongoDBStore"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"npm install mongodb@5\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"index.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { createApp, ServiceManager } from '@foal/core';\nimport { MongoDBStore } from '@foal/mongodb';\nimport { MongoClient } from 'mongodb';\n\nasync function main() {\n const mongoDBClient = await MongoClient.connect('mongodb://localhost:27017/db');\n\n const serviceManager = new ServiceManager();\n serviceManager.get(MongoDBStore).setMongoDBClient(mongoDBClient);\n\n const app = await createApp(AppController, { serviceManager });\n\n // ...\n}\n"})})]})}function p(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},19365:(e,n,s)=>{s.d(n,{A:()=>r});s(96540);var t=s(34164);const o={tabItem:"tabItem_Ymn6"};var i=s(74848);function r(e){let{children:n,hidden:s,className:r}=e;return(0,i.jsx)("div",{role:"tabpanel",className:(0,t.A)(o.tabItem,r),hidden:s,children:n})}},11470:(e,n,s)=>{s.d(n,{A:()=>S});var t=s(96540),o=s(34164),i=s(23104),r=s(56347),a=s(205),c=s(57485),l=s(31682),d=s(89466);function h(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function u(e){const{values:n,children:s}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return h(e).map((e=>{let{props:{value:n,label:s,attributes:t,default:o}}=e;return{value:n,label:s,attributes:t,default:o}}))}(s);return function(e){const n=(0,l.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,s])}function p(e){let{value:n,tabValues:s}=e;return s.some((e=>e.value===n))}function x(e){let{queryString:n=!1,groupId:s}=e;const o=(0,r.W6)(),i=function(e){let{queryString:n=!1,groupId:s}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!s)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return s??null}({queryString:n,groupId:s});return[(0,c.aZ)(i),(0,t.useCallback)((e=>{if(!i)return;const n=new URLSearchParams(o.location.search);n.set(i,e),o.replace({...o.location,search:n.toString()})}),[i,o])]}function m(e){const{defaultValue:n,queryString:s=!1,groupId:o}=e,i=u(e),[r,c]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:s}=e;if(0===s.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:s}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${s.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=s.find((e=>e.default))??s[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:i}))),[l,h]=x({queryString:s,groupId:o}),[m,j]=function(e){let{groupId:n}=e;const s=function(e){return e?`docusaurus.tab.${e}`:null}(n),[o,i]=(0,d.Dv)(s);return[o,(0,t.useCallback)((e=>{s&&i.set(e)}),[s,i])]}({groupId:o}),g=(()=>{const e=l??m;return p({value:e,tabValues:i})?e:null})();(0,a.A)((()=>{g&&c(g)}),[g]);return{selectedValue:r,selectValue:(0,t.useCallback)((e=>{if(!p({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);c(e),h(e),j(e)}),[h,j,i]),tabValues:i}}var j=s(92303);const g={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var f=s(74848);function y(e){let{className:n,block:s,selectedValue:t,selectValue:r,tabValues:a}=e;const c=[],{blockElementScrollPositionUntilNextRender:l}=(0,i.a_)(),d=e=>{const n=e.currentTarget,s=c.indexOf(n),o=a[s].value;o!==t&&(l(n),r(o))},h=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const s=c.indexOf(e.currentTarget)+1;n=c[s]??c[0];break}case"ArrowLeft":{const s=c.indexOf(e.currentTarget)-1;n=c[s]??c[c.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,o.A)("tabs",{"tabs--block":s},n),children:a.map((e=>{let{value:n,label:s,attributes:i}=e;return(0,f.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>c.push(e),onKeyDown:h,onClick:d,...i,className:(0,o.A)("tabs__item",g.tabItem,i?.className,{"tabs__item--active":t===n}),children:s??n},n)}))})}function v(e){let{lazy:n,children:s,selectedValue:o}=e;const i=(Array.isArray(s)?s:[s]).filter(Boolean);if(n){const e=i.find((e=>e.props.value===o));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:i.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==o})))})}function b(e){const n=m(e);return(0,f.jsxs)("div",{className:(0,o.A)("tabs-container",g.tabList),children:[(0,f.jsx)(y,{...e,...n}),(0,f.jsx)(v,{...e,...n})]})}function S(e){const n=(0,j.A)();return(0,f.jsx)(b,{...e,children:h(e.children)},String(n))}},66674:(e,n,s)=>{s.d(n,{A:()=>t});const t=s.p+"assets/images/user-cookie-c3f0e06d9ced0800240ac2b5ee368b8c.png"},28453:(e,n,s)=>{s.d(n,{R:()=>r,x:()=>a});var t=s(96540);const o={},i=t.createContext(o);function r(e){const n=t.useContext(i);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/d9c83d6c.f7699cac.js b/assets/js/d9c83d6c.ef39065a.js similarity index 84% rename from assets/js/d9c83d6c.f7699cac.js rename to assets/js/d9c83d6c.ef39065a.js index d56c415245..4decddffbf 100644 --- a/assets/js/d9c83d6c.f7699cac.js +++ b/assets/js/d9c83d6c.ef39065a.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[639],{88063:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var s=t(74848),o=t(28453);const i={title:"Using Another ORM",sidebar_label:"Introduction"},r=void 0,a={id:"databases/other-orm/introduction",title:"Using Another ORM",description:"The core of the framework is independent of TypeORM. So, if you do not want to use an ORM at all or use another ORM or ODM than TypeORM, you absolutely can.",source:"@site/docs/databases/other-orm/introduction.md",sourceDirName:"databases/other-orm",slug:"/databases/other-orm/introduction",permalink:"/docs/databases/other-orm/introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/databases/other-orm/introduction.md",tags:[],version:"current",frontMatter:{title:"Using Another ORM",sidebar_label:"Introduction"},sidebar:"someSidebar",previous:{title:"NoSQL",permalink:"/docs/databases/typeorm/mongodb"},next:{title:"Prisma",permalink:"/docs/databases/other-orm/prisma"}},l={},d=[{value:"Uninstall TypeORM",id:"uninstall-typeorm",level:2},{value:"Examples",id:"examples",level:2},{value:"Limitations",id:"limitations",level:2}];function c(e){const n={a:"a",code:"code",em:"em",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"The core of the framework is independent of TypeORM. So, if you do not want to use an ORM at all or use another ORM or ODM than TypeORM, you absolutely can."}),"\n",(0,s.jsx)(n.p,{children:"To do so, you will have to remove TypeORM and all its utilities."}),"\n",(0,s.jsx)(n.h2,{id:"uninstall-typeorm",children:"Uninstall TypeORM"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"First uninstall the dependencies."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm uninstall typeorm @foal/typeorm\n"})}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Then remove the directory ",(0,s.jsx)(n.code,{children:"src/app/entities"})," and the file ",(0,s.jsx)(n.code,{children:"src/db.ts"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Remove or replace the script ",(0,s.jsx)(n.code,{children:"create-user"})," in ",(0,s.jsx)(n.code,{children:"src/app/scripts"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["In the file ",(0,s.jsx)(n.code,{children:"src/index.ts"}),", delete the connection creation called ",(0,s.jsx)(n.code,{children:"dataSource.initialize()"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Finally, remove in ",(0,s.jsx)(n.code,{children:"package.json"})," the scripts to manage migrations."]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"/docs/databases/other-orm/prisma",children:"Prisma"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"limitations",children:"Limitations"}),"\n",(0,s.jsx)(n.p,{children:"When using another ORM than TypeORM some features are not available:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.em,{children:"Groups & Permissions"})," system,"]}),"\n",(0,s.jsxs)(n.li,{children:["and the ",(0,s.jsx)(n.code,{children:"foal g rest-api"})," command."]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>a});var s=t(96540);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[639],{88063:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var s=t(74848),o=t(28453);const i={title:"Using Another ORM",sidebar_label:"Introduction"},r=void 0,a={id:"databases/other-orm/introduction",title:"Using Another ORM",description:"The core of the framework is independent of TypeORM. So, if you do not want to use an ORM at all or use another ORM or ODM than TypeORM, you absolutely can.",source:"@site/docs/databases/other-orm/introduction.md",sourceDirName:"databases/other-orm",slug:"/databases/other-orm/introduction",permalink:"/docs/databases/other-orm/introduction",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/databases/other-orm/introduction.md",tags:[],version:"current",frontMatter:{title:"Using Another ORM",sidebar_label:"Introduction"},sidebar:"someSidebar",previous:{title:"NoSQL",permalink:"/docs/databases/typeorm/mongodb"},next:{title:"Prisma",permalink:"/docs/databases/other-orm/prisma"}},l={},d=[{value:"Uninstall TypeORM",id:"uninstall-typeorm",level:2},{value:"Examples",id:"examples",level:2},{value:"Limitations",id:"limitations",level:2}];function c(e){const n={a:"a",code:"code",em:"em",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.p,{children:"The core of the framework is independent of TypeORM. So, if you do not want to use an ORM at all or use another ORM or ODM than TypeORM, you absolutely can."}),"\n",(0,s.jsx)(n.p,{children:"To do so, you will have to remove TypeORM and all its utilities."}),"\n",(0,s.jsx)(n.h2,{id:"uninstall-typeorm",children:"Uninstall TypeORM"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"First uninstall the dependencies."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"npm uninstall typeorm @foal/typeorm\n"})}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Then remove the directory ",(0,s.jsx)(n.code,{children:"src/app/entities"})," and the file ",(0,s.jsx)(n.code,{children:"src/db.ts"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Remove or replace the script ",(0,s.jsx)(n.code,{children:"create-user"})," in ",(0,s.jsx)(n.code,{children:"src/app/scripts"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["In the file ",(0,s.jsx)(n.code,{children:"src/index.ts"}),", delete the connection creation called ",(0,s.jsx)(n.code,{children:"dataSource.initialize()"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Finally, remove in ",(0,s.jsx)(n.code,{children:"package.json"})," the scripts to manage migrations."]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.a,{href:"/docs/databases/other-orm/prisma",children:"Prisma"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"limitations",children:"Limitations"}),"\n",(0,s.jsx)(n.p,{children:"When using another ORM than TypeORM some features are not available:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["the ",(0,s.jsx)(n.em,{children:"Groups & Permissions"})," system,"]}),"\n",(0,s.jsxs)(n.li,{children:["and the ",(0,s.jsx)(n.code,{children:"npx foal g rest-api"})," command."]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},28453:(e,n,t)=>{t.d(n,{R:()=>r,x:()=>a});var s=t(96540);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/daacca3b.1ff1d5dd.js b/assets/js/daacca3b.1ff1d5dd.js new file mode 100644 index 0000000000..a5900dd96c --- /dev/null +++ b/assets/js/daacca3b.1ff1d5dd.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8389],{22816:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>i,default:()=>p,frontMatter:()=>l,metadata:()=>a,toc:()=>d});var r=n(74848),o=n(28453);const l={title:"Production Build",id:"tuto-14-production-build",slug:"14-production-build"},i=void 0,a={id:"tutorials/real-world-example-with-react/tuto-14-production-build",title:"Production Build",description:"So far, the front-end and back-end applications are compiled and served by two different development servers. The next step is to build them into a single one ready for production.",source:"@site/docs/tutorials/real-world-example-with-react/14-production-build.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/14-production-build",permalink:"/docs/tutorials/real-world-example-with-react/14-production-build",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/14-production-build.md",tags:[],version:"current",sidebarPosition:14,frontMatter:{title:"Production Build",id:"tuto-14-production-build",slug:"14-production-build"},sidebar:"someSidebar",previous:{title:"CSRF Protection",permalink:"/docs/tutorials/real-world-example-with-react/13-csrf"},next:{title:"Social Auth with Google",permalink:"/docs/tutorials/real-world-example-with-react/15-social-auth"}},s={},d=[{value:"Building the React app",id:"building-the-react-app",level:2},{value:"Preventing 404 errors",id:"preventing-404-errors",level:2},{value:"Building the Foal app",id:"building-the-foal-app",level:2}];function c(e){const t={a:"a",code:"code",h2:"h2",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.p,{children:"So far, the front-end and back-end applications are compiled and served by two different development servers. The next step is to build them into a single one ready for production."}),"\n",(0,r.jsx)(t.h2,{id:"building-the-react-app",children:"Building the React app"}),"\n",(0,r.jsx)(t.p,{children:"In your frontend directory, run the following command:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,r.jsxs)(t.p,{children:["This command builds the React application for production and saves the files in the ",(0,r.jsx)(t.code,{children:"public"})," directory of your Foal application."]}),"\n",(0,r.jsxs)(t.p,{children:["Now, if you navigate to ",(0,r.jsx)(t.a,{href:"http://localhost:3001",children:"http://localhost:3001"}),", you will see the frontend application served by the backend server."]}),"\n",(0,r.jsx)(t.h2,{id:"preventing-404-errors",children:"Preventing 404 errors"}),"\n",(0,r.jsxs)(t.p,{children:["Open the link ",(0,r.jsx)(t.a,{href:"http://localhost:3001/login",children:"http://localhost:3001/login"})," in a new tab. The server returns a 404 error."]}),"\n",(0,r.jsxs)(t.p,{children:["This is perfectly normal. At the moment, the server does not have a handler for the ",(0,r.jsx)(t.code,{children:"/login"})," route and therefore returns this error. Previously, this problem was handled by the React development server, which is why there was no such error."]}),"\n",(0,r.jsx)(t.p,{children:"To solve this problem, you will add a controller method that will process unhandled requests."}),"\n",(0,r.jsxs)(t.p,{children:["Open ",(0,r.jsx)(t.code,{children:"app.controller.ts"})," and update its contents."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { Context, controller, Get, HttpResponseNotFound, IAppController, render } from '@foal/core';\n\nimport { ApiController, OpenapiController } from './controllers';\n\nexport class AppController implements IAppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenapiController)\n ];\n\n @Get('*')\n renderApp(ctx: Context) {\n if (!ctx.request.accepts('html')) {\n return new HttpResponseNotFound();\n }\n\n return render('./public/index.html');\n }\n}\n\n"})}),"\n",(0,r.jsx)(t.p,{children:"This method returns the React application for any GET request that accepts HTML content and has not been handled by the other methods of the controller and its subcontrollers."}),"\n",(0,r.jsxs)(t.p,{children:["If you return to ",(0,r.jsx)(t.a,{href:"http://localhost:3001/login",children:"http://localhost:3001/login"})," and refresh the page, the login page should display."]}),"\n",(0,r.jsx)(t.h2,{id:"building-the-foal-app",children:"Building the Foal app"}),"\n",(0,r.jsxs)(t.p,{children:["Now, if you want to build the backend application so that you don't use the ",(0,r.jsx)(t.code,{children:"npm run dev"})," option, you can run this command:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,r.jsx)(t.p,{children:"Then, to launch the application, simply execute the following:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run start\n"})})]})}function p(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>i,x:()=>a});var r=n(96540);const o={},l=r.createContext(o);function i(e){const t=r.useContext(l);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),r.createElement(l.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/daacca3b.4b8853b3.js b/assets/js/daacca3b.4b8853b3.js deleted file mode 100644 index 3fad56694f..0000000000 --- a/assets/js/daacca3b.4b8853b3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8389],{22816:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>a,contentTitle:()=>i,default:()=>p,frontMatter:()=>l,metadata:()=>s,toc:()=>d});var o=n(74848),r=n(28453);const l={title:"Production Build",id:"tuto-14-production-build",slug:"14-production-build"},i=void 0,s={id:"tutorials/real-world-example-with-react/tuto-14-production-build",title:"Production Build",description:"So far, the front-end and back-end applications are compiled and served by two different development servers. The next step is to build them into a single one ready for production.",source:"@site/docs/tutorials/real-world-example-with-react/14-production-build.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/14-production-build",permalink:"/docs/tutorials/real-world-example-with-react/14-production-build",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/14-production-build.md",tags:[],version:"current",sidebarPosition:14,frontMatter:{title:"Production Build",id:"tuto-14-production-build",slug:"14-production-build"},sidebar:"someSidebar",previous:{title:"CSRF Protection",permalink:"/docs/tutorials/real-world-example-with-react/13-csrf"},next:{title:"Social Auth with Google",permalink:"/docs/tutorials/real-world-example-with-react/15-social-auth"}},a={},d=[{value:"Building the React app",id:"building-the-react-app",level:2},{value:"Preventing 404 errors",id:"preventing-404-errors",level:2},{value:"Building the Foal app",id:"building-the-foal-app",level:2}];function c(e){const t={a:"a",blockquote:"blockquote",code:"code",h2:"h2",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.p,{children:"So far, the front-end and back-end applications are compiled and served by two different development servers. The next step is to build them into a single one ready for production."}),"\n",(0,o.jsx)(t.h2,{id:"building-the-react-app",children:"Building the React app"}),"\n",(0,o.jsx)(t.p,{children:"In your frontend directory, run the following command:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,o.jsxs)(t.p,{children:["This command builds the React application for production and saves the files in the ",(0,o.jsx)(t.code,{children:"build"})," directory."]}),"\n",(0,o.jsxs)(t.p,{children:["Open it and copy all its contents to the ",(0,o.jsx)(t.code,{children:"public"})," directory of your Foal application."]}),"\n",(0,o.jsxs)(t.blockquote,{children:["\n",(0,o.jsxs)(t.p,{children:["When you use ",(0,o.jsx)(t.code,{children:"foal connect"})," with Angular or Vue, the frontend build will automatically save the files in ",(0,o.jsx)(t.code,{children:"public"}),"."]}),"\n"]}),"\n",(0,o.jsxs)(t.p,{children:["Now, if you navigate to ",(0,o.jsx)(t.a,{href:"http://localhost:3001",children:"http://localhost:3001"}),", you will see the frontend application served by the backend server."]}),"\n",(0,o.jsx)(t.h2,{id:"preventing-404-errors",children:"Preventing 404 errors"}),"\n",(0,o.jsxs)(t.p,{children:["Open the link ",(0,o.jsx)(t.a,{href:"http://localhost:3001/login",children:"http://localhost:3001/login"})," in a new tab. The server returns a 404 error."]}),"\n",(0,o.jsxs)(t.p,{children:["This is perfectly normal. At the moment, the server does not have a handler for the ",(0,o.jsx)(t.code,{children:"/login"})," route and therefore returns this error. Previously, this problem was handled by the React development server, which is why there was no such error."]}),"\n",(0,o.jsx)(t.p,{children:"To solve this problem, you will add a controller method that will process unhandled requests."}),"\n",(0,o.jsxs)(t.p,{children:["Open ",(0,o.jsx)(t.code,{children:"app.controller.ts"})," and update its contents."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"import { Context, controller, Get, HttpResponseNotFound, IAppController, render } from '@foal/core';\n\nimport { ApiController, OpenapiController } from './controllers';\n\nexport class AppController implements IAppController {\n subControllers = [\n controller('/api', ApiController),\n controller('/swagger', OpenapiController)\n ];\n\n @Get('*')\n renderApp(ctx: Context) {\n if (!ctx.request.accepts('html')) {\n return new HttpResponseNotFound();\n }\n\n return render('./public/index.html');\n }\n}\n\n"})}),"\n",(0,o.jsx)(t.p,{children:"This method returns the React application for any GET request that accepts HTML content and has not been handled by the other methods of the controller and its subcontrollers."}),"\n",(0,o.jsxs)(t.p,{children:["If you return to ",(0,o.jsx)(t.a,{href:"http://localhost:3001/login",children:"http://localhost:3001/login"})," and refresh the page, the login page should display."]}),"\n",(0,o.jsx)(t.h2,{id:"building-the-foal-app",children:"Building the Foal app"}),"\n",(0,o.jsxs)(t.p,{children:["Now, if you want to build the backend application so that you don't use the ",(0,o.jsx)(t.code,{children:"npm run dev"})," option, you can run this command:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"npm run build\n"})}),"\n",(0,o.jsx)(t.p,{children:"Then, to launch the application, simply execute the following:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-bash",children:"npm run start\n"})})]})}function p(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>i,x:()=>s});var o=n(96540);const r={},l=o.createContext(r);function i(e){const t=o.useContext(l);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),o.createElement(l.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/dcd6df2d.bd1091e1.js b/assets/js/dcd6df2d.bd1091e1.js new file mode 100644 index 0000000000..7728d78f16 --- /dev/null +++ b/assets/js/dcd6df2d.bd1091e1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5914],{52139:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>i,contentTitle:()=>a,default:()=>m,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var n=o(74848),r=o(28453);const s={title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},a=void 0,l={permalink:"/blog/2024/04/25/version-4.4-release-notes",editUrl:"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-25-version-4.4-release-notes.md",source:"@site/blog/2024-04-25-version-4.4-release-notes.md",title:"Version 4.4 release notes",description:"Banner",date:"2024-04-25T00:00:00.000Z",formattedDate:"April 25, 2024",tags:[{label:"release",permalink:"/blog/tags/release"}],readingTime:.19,hasTruncateMarker:!0,authors:[{name:"Lo\xefc Poullain",title:"Creator of FoalTS. Software engineer.",url:"https://loicpoullain.com",imageURL:"https://avatars1.githubusercontent.com/u/13604533?v=4"}],frontMatter:{title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},unlisted:!1,prevItem:{title:"Version 4.5 release notes",permalink:"/blog/2024/08/22/version-4.5-release-notes"},nextItem:{title:"Version 4.3 release notes",permalink:"/blog/2024/04/16/version-4.3-release-notes"}},i={authorsImageUrls:[void 0]},c=[];function u(e){const t={a:"a",img:"img",p:"p",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Banner",src:o(7195).A+"",width:"914",height:"315"})}),"\n",(0,n.jsxs)(t.p,{children:["Version 4.4 of ",(0,n.jsx)(t.a,{href:"https://foalts.org/",children:"Foal"})," is out!"]})]})}function m(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(u,{...e})}):u(e)}},7195:(e,t,o)=>{o.d(t,{A:()=>n});const n=o.p+"assets/images/banner-33e9e1af8b06d33e1bedef8fc0c5071c.png"},28453:(e,t,o)=>{o.d(t,{R:()=>a,x:()=>l});var n=o(96540);const r={},s=n.createContext(r);function a(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function l(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/dcd6df2d.c0fdb195.js b/assets/js/dcd6df2d.c0fdb195.js deleted file mode 100644 index d59e7094c7..0000000000 --- a/assets/js/dcd6df2d.c0fdb195.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[5914],{52139:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>m,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var n=o(74848),r=o(28453);const s={title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},a=void 0,i={permalink:"/blog/2024/04/25/version-4.4-release-notes",editUrl:"https://github.com/FoalTS/foal/edit/master/docs/blog/2024-04-25-version-4.4-release-notes.md",source:"@site/blog/2024-04-25-version-4.4-release-notes.md",title:"Version 4.4 release notes",description:"Banner",date:"2024-04-25T00:00:00.000Z",formattedDate:"April 25, 2024",tags:[{label:"release",permalink:"/blog/tags/release"}],readingTime:.19,hasTruncateMarker:!0,authors:[{name:"Lo\xefc Poullain",title:"Creator of FoalTS. Software engineer.",url:"https://loicpoullain.com",imageURL:"https://avatars1.githubusercontent.com/u/13604533?v=4"}],frontMatter:{title:"Version 4.4 release notes",author:"Lo\xefc Poullain",author_title:"Creator of FoalTS. Software engineer.",author_url:"https://loicpoullain.com",author_image_url:"https://avatars1.githubusercontent.com/u/13604533?v=4",image:"blog/twitter-banners/version-4.4-release-notes.png",tags:["release"]},unlisted:!1,nextItem:{title:"Version 4.3 release notes",permalink:"/blog/2024/04/16/version-4.3-release-notes"}},l={authorsImageUrls:[void 0]},c=[];function u(e){const t={a:"a",img:"img",p:"p",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Banner",src:o(7195).A+"",width:"914",height:"315"})}),"\n",(0,n.jsxs)(t.p,{children:["Version 4.4 of ",(0,n.jsx)(t.a,{href:"https://foalts.org/",children:"Foal"})," is out!"]})]})}function m(e={}){const{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(u,{...e})}):u(e)}},7195:(e,t,o)=>{o.d(t,{A:()=>n});const n=o.p+"assets/images/banner-33e9e1af8b06d33e1bedef8fc0c5071c.png"},28453:(e,t,o)=>{o.d(t,{R:()=>a,x:()=>i});var n=o(96540);const r={},s=n.createContext(r);function a(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/dd6459f3.e5468757.js b/assets/js/dd6459f3.0723b77a.js similarity index 72% rename from assets/js/dd6459f3.e5468757.js rename to assets/js/dd6459f3.0723b77a.js index c09ddc17e7..f5809155e7 100644 --- a/assets/js/dd6459f3.e5468757.js +++ b/assets/js/dd6459f3.0723b77a.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4466],{59491:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>i,metadata:()=>d,toc:()=>l});var r=t(74848),s=t(28453);const i={title:"REST API"},o=void 0,d={id:"common/rest-blueprints",title:"REST API",description:"Example:",source:"@site/docs/common/rest-blueprints.md",sourceDirName:"common",slug:"/common/rest-blueprints",permalink:"/docs/common/rest-blueprints",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/rest-blueprints.md",tags:[],version:"current",frontMatter:{title:"REST API"},sidebar:"someSidebar",previous:{title:"Task Scheduling",permalink:"/docs/common/task-scheduling"},next:{title:"OpenAPI",permalink:"/docs/common/openapi-and-swagger-ui"}},c={},l=[{value:"The API Behavior",id:"the-api-behavior",level:2},{value:"The Resource and its Representation",id:"the-resource-and-its-representation",level:2},{value:"How to Add New Field",id:"how-to-add-new-field",level:3},{value:"Using Authentication",id:"using-authentication",level:2},{value:"Generating OpenAPI documentation",id:"generating-openapi-documentation",level:2}];function a(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"foal generate rest-api product --register\n"})}),"\n",(0,r.jsx)(n.p,{children:"Building a REST API is often a common task when creating an application. To avoid reinventing the wheel, FoalTS provides a CLI command to achieve this."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"foal generate rest-api [--register] [--auth]\n"})}),"\n",(0,r.jsx)(n.p,{children:"This command generates three files: an entity, a controller and the controller's test. Depending on your directory structure, they may be generated in different locations:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["in the directories ",(0,r.jsx)(n.code,{children:"src/app/controllers"})," and ",(0,r.jsx)(n.code,{children:"src/app/entities"})," if they exist"]}),"\n",(0,r.jsxs)(n.li,{children:["or in the directories ",(0,r.jsx)(n.code,{children:"controllers"})," and ",(0,r.jsx)(n.code,{children:"entities"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The generated controller already has a set of implemented routes ",(0,r.jsx)(n.strong,{children:"that you can customize as you like"}),". It defines a REST API and is ready to use. The only thing to do is to connect the controller to the ",(0,r.jsx)(n.code,{children:"AppController"})," or one of its children."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"--register"})," option automatically registers your controller in the ",(0,r.jsx)(n.code,{children:"AppController"}),"."]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"the-api-behavior",children:"The API Behavior"}),"\n",(0,r.jsx)(n.p,{children:"Below is a table summarizing how the generated API works:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.em,{children:"HTTP Method"})}),(0,r.jsx)(n.th,{children:(0,r.jsx)(n.em,{children:"CRUD"})}),(0,r.jsx)(n.th,{children:(0,r.jsxs)(n.em,{children:["Entire Collection (e.g. ",(0,r.jsx)(n.code,{children:"/products"}),")"]})}),(0,r.jsx)(n.th,{children:(0,r.jsxs)(n.em,{children:["Specific Item (e.g. ",(0,r.jsx)(n.code,{children:"/products/{id}"}),")"]})})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GET"}),(0,r.jsx)(n.td,{children:"Read"}),(0,r.jsx)(n.td,{children:"200 (OK) - list of products"}),(0,r.jsxs)(n.td,{children:["200 (OK) - the product ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"POST"}),(0,r.jsx)(n.td,{children:"Create"}),(0,r.jsxs)(n.td,{children:["201 (Created) - the created product ",(0,r.jsx)("br",{})," 400 (Bad Request) - the validation error"]}),(0,r.jsx)(n.td,{children:"Not implemented"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PUT"}),(0,r.jsx)(n.td,{children:"Update/Replace"}),(0,r.jsx)(n.td,{children:"Not implemented"}),(0,r.jsxs)(n.td,{children:["200 (OK) - the updated product ",(0,r.jsx)("br",{})," 400 (Bad Request) - the validation error ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PATCH"}),(0,r.jsx)(n.td,{children:"Update/Modify"}),(0,r.jsx)(n.td,{children:"Not implemented"}),(0,r.jsxs)(n.td,{children:["200 (OK) - the updated product ",(0,r.jsx)("br",{})," 400 (Bad Request) - the validation error ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"DELETE"}),(0,r.jsx)(n.td,{children:"Delete"}),(0,r.jsx)(n.td,{children:"Not implemented"}),(0,r.jsxs)(n.td,{children:["204 (No Content) ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"GET /s"})," routes also accept two optional query parameters ",(0,r.jsx)(n.code,{children:"skip"})," and ",(0,r.jsx)(n.code,{children:"take"})," to handle ",(0,r.jsx)(n.strong,{children:"pagination"}),". If the parameters are not valid numbers, the controller responds with a 400 (Bad Request) status."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"skip"})," - offset from where items should be taken."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"take"})," - max number of items that should be taken."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /products?skip=10&take=20\n"})}),"\n",(0,r.jsx)(n.h2,{id:"the-resource-and-its-representation",children:"The Resource and its Representation"}),"\n",(0,r.jsx)(n.p,{children:"Once your API is set up, you can define its attributes."}),"\n",(0,r.jsx)(n.p,{children:"The entity generated by default should look like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Product extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n text: string;\n\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"And the schema of your API (defined in the controller file) should look like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"export const productSchhema = {\n additionalProperties: false,\n properties: {\n text: { type: 'string', maxLength: 255 },\n },\n required: [ 'text' ],\n type: 'object',\n};\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The entity is the ",(0,r.jsx)(n.em,{children:"resource"}),". It is the database model used internally on the server."]}),"\n",(0,r.jsxs)(n.p,{children:["The schema is the ",(0,r.jsx)(n.em,{children:"representation of the resource"}),". It defines the interface of the API."]}),"\n",(0,r.jsx)(n.p,{children:"In simple scenarios, the two are very similar but they can differ much more in complex applications. A resource may have several representations and it may be made of several entities."}),"\n",(0,r.jsx)(n.h3,{id:"how-to-add-new-field",children:"How to Add New Field"}),"\n",(0,r.jsxs)(n.p,{children:["For example, if you want to add a ",(0,r.jsx)(n.code,{children:"sold"})," boolean field whose default value is ",(0,r.jsx)(n.code,{children:"false"}),", you can do the following:"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"product.entity.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Product extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n text: string;\n\n @Column()\n sold: boolean;\n\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"product.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"export const productSchhema = {\n additionalProperties: false,\n properties: {\n sold: { type: 'boolean', default: false },\n text: { type: 'string', maxLength: 255 },\n },\n required: [ 'text' ],\n type: 'object',\n};\n"})}),"\n",(0,r.jsx)(n.h2,{id:"using-authentication",children:"Using Authentication"}),"\n",(0,r.jsxs)(n.p,{children:["If you wish to attach a user to the resource, you can use the ",(0,r.jsx)(n.code,{children:"--auth"})," flag to do so."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"foal generate rest-api product --auth\n"})}),"\n",(0,r.jsxs)(n.p,{children:["This flags adds an ",(0,r.jsx)(n.code,{children:"owner: User"})," column to your entity and uses it in the API."]}),"\n",(0,r.jsx)(n.h2,{id:"generating-openapi-documentation",children:"Generating OpenAPI documentation"}),"\n",(0,r.jsx)(n.p,{children:"The generated controllers also have OpenAPI decorators on their methods to document the API."}),"\n",(0,r.jsxs)(n.p,{children:["In this way, when the ",(0,r.jsx)(n.a,{href:"/docs/architecture/configuration",children:"configuration key"})," ",(0,r.jsx)(n.code,{children:"settings.openapi.useHooks"})," is set to ",(0,r.jsx)(n.code,{children:"true"}),", we can get a full documentation of the API using ",(0,r.jsx)(n.a,{href:"/docs/common/openapi-and-swagger-ui",children:"Swagger UI"})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.img,{alt:"Example of documentation",src:t(80958).A+"",width:"2538",height:"1298"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},80958:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/rest-openapi-28b755aa0e71f5a2a6ec387ea2c8ec98.png"},28453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>d});var r=t(96540);const s={},i=r.createContext(s);function o(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[4466],{59491:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>i,metadata:()=>d,toc:()=>l});var r=t(74848),s=t(28453);const i={title:"REST API"},o=void 0,d={id:"common/rest-blueprints",title:"REST API",description:"Example:",source:"@site/docs/common/rest-blueprints.md",sourceDirName:"common",slug:"/common/rest-blueprints",permalink:"/docs/common/rest-blueprints",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/common/rest-blueprints.md",tags:[],version:"current",frontMatter:{title:"REST API"},sidebar:"someSidebar",previous:{title:"Async tasks",permalink:"/docs/common/async-tasks"},next:{title:"OpenAPI",permalink:"/docs/common/openapi-and-swagger-ui"}},c={},l=[{value:"The API Behavior",id:"the-api-behavior",level:2},{value:"The Resource and its Representation",id:"the-resource-and-its-representation",level:2},{value:"How to Add New Field",id:"how-to-add-new-field",level:3},{value:"Using Authentication",id:"using-authentication",level:2},{value:"Generating OpenAPI documentation",id:"generating-openapi-documentation",level:2}];function a(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npx foal generate rest-api product --register\n"})}),"\n",(0,r.jsx)(n.p,{children:"Building a REST API is often a common task when creating an application. To avoid reinventing the wheel, FoalTS provides a CLI command to achieve this."}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npx foal generate rest-api [--register] [--auth]\n"})}),"\n",(0,r.jsx)(n.p,{children:"This command generates three files: an entity, a controller and the controller's test. Depending on your directory structure, they may be generated in different locations:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["in the directories ",(0,r.jsx)(n.code,{children:"src/app/controllers"})," and ",(0,r.jsx)(n.code,{children:"src/app/entities"})," if they exist"]}),"\n",(0,r.jsxs)(n.li,{children:["or in the directories ",(0,r.jsx)(n.code,{children:"controllers"})," and ",(0,r.jsx)(n.code,{children:"entities"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["The generated controller already has a set of implemented routes ",(0,r.jsx)(n.strong,{children:"that you can customize as you like"}),". It defines a REST API and is ready to use. The only thing to do is to connect the controller to the ",(0,r.jsx)(n.code,{children:"AppController"})," or one of its children."]}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"--register"})," option automatically registers your controller in the ",(0,r.jsx)(n.code,{children:"AppController"}),"."]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"the-api-behavior",children:"The API Behavior"}),"\n",(0,r.jsx)(n.p,{children:"Below is a table summarizing how the generated API works:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.em,{children:"HTTP Method"})}),(0,r.jsx)(n.th,{children:(0,r.jsx)(n.em,{children:"CRUD"})}),(0,r.jsx)(n.th,{children:(0,r.jsxs)(n.em,{children:["Entire Collection (e.g. ",(0,r.jsx)(n.code,{children:"/products"}),")"]})}),(0,r.jsx)(n.th,{children:(0,r.jsxs)(n.em,{children:["Specific Item (e.g. ",(0,r.jsx)(n.code,{children:"/products/{id}"}),")"]})})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"GET"}),(0,r.jsx)(n.td,{children:"Read"}),(0,r.jsx)(n.td,{children:"200 (OK) - list of products"}),(0,r.jsxs)(n.td,{children:["200 (OK) - the product ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"POST"}),(0,r.jsx)(n.td,{children:"Create"}),(0,r.jsxs)(n.td,{children:["201 (Created) - the created product ",(0,r.jsx)("br",{})," 400 (Bad Request) - the validation error"]}),(0,r.jsx)(n.td,{children:"Not implemented"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PUT"}),(0,r.jsx)(n.td,{children:"Update/Replace"}),(0,r.jsx)(n.td,{children:"Not implemented"}),(0,r.jsxs)(n.td,{children:["200 (OK) - the updated product ",(0,r.jsx)("br",{})," 400 (Bad Request) - the validation error ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"PATCH"}),(0,r.jsx)(n.td,{children:"Update/Modify"}),(0,r.jsx)(n.td,{children:"Not implemented"}),(0,r.jsxs)(n.td,{children:["200 (OK) - the updated product ",(0,r.jsx)("br",{})," 400 (Bad Request) - the validation error ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"DELETE"}),(0,r.jsx)(n.td,{children:"Delete"}),(0,r.jsx)(n.td,{children:"Not implemented"}),(0,r.jsxs)(n.td,{children:["204 (No Content) ",(0,r.jsx)("br",{})," 404 (Not Found)"]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"GET /s"})," routes also accept two optional query parameters ",(0,r.jsx)(n.code,{children:"skip"})," and ",(0,r.jsx)(n.code,{children:"take"})," to handle ",(0,r.jsx)(n.strong,{children:"pagination"}),". If the parameters are not valid numbers, the controller responds with a 400 (Bad Request) status."]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"skip"})," - offset from where items should be taken."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"take"})," - max number of items that should be taken."]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"GET /products?skip=10&take=20\n"})}),"\n",(0,r.jsx)(n.h2,{id:"the-resource-and-its-representation",children:"The Resource and its Representation"}),"\n",(0,r.jsx)(n.p,{children:"Once your API is set up, you can define its attributes."}),"\n",(0,r.jsx)(n.p,{children:"The entity generated by default should look like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Product extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n text: string;\n\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"And the schema of your API (defined in the controller file) should look like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"export const productSchhema = {\n additionalProperties: false,\n properties: {\n text: { type: 'string', maxLength: 255 },\n },\n required: [ 'text' ],\n type: 'object',\n};\n"})}),"\n",(0,r.jsxs)(n.p,{children:["The entity is the ",(0,r.jsx)(n.em,{children:"resource"}),". It is the database model used internally on the server."]}),"\n",(0,r.jsxs)(n.p,{children:["The schema is the ",(0,r.jsx)(n.em,{children:"representation of the resource"}),". It defines the interface of the API."]}),"\n",(0,r.jsx)(n.p,{children:"In simple scenarios, the two are very similar but they can differ much more in complex applications. A resource may have several representations and it may be made of several entities."}),"\n",(0,r.jsx)(n.h3,{id:"how-to-add-new-field",children:"How to Add New Field"}),"\n",(0,r.jsxs)(n.p,{children:["For example, if you want to add a ",(0,r.jsx)(n.code,{children:"sold"})," boolean field whose default value is ",(0,r.jsx)(n.code,{children:"false"}),", you can do the following:"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"product.entity.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class Product extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n text: string;\n\n @Column()\n sold: boolean;\n\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"product.controller.ts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"export const productSchhema = {\n additionalProperties: false,\n properties: {\n sold: { type: 'boolean', default: false },\n text: { type: 'string', maxLength: 255 },\n },\n required: [ 'text' ],\n type: 'object',\n};\n"})}),"\n",(0,r.jsx)(n.h2,{id:"using-authentication",children:"Using Authentication"}),"\n",(0,r.jsxs)(n.p,{children:["If you wish to attach a user to the resource, you can use the ",(0,r.jsx)(n.code,{children:"--auth"})," flag to do so."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.em,{children:"Example:"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"npx foal generate rest-api product --auth\n"})}),"\n",(0,r.jsxs)(n.p,{children:["This flags adds an ",(0,r.jsx)(n.code,{children:"owner: User"})," column to your entity and uses it in the API."]}),"\n",(0,r.jsx)(n.h2,{id:"generating-openapi-documentation",children:"Generating OpenAPI documentation"}),"\n",(0,r.jsx)(n.p,{children:"The generated controllers also have OpenAPI decorators on their methods to document the API."}),"\n",(0,r.jsxs)(n.p,{children:["In this way, when the ",(0,r.jsx)(n.a,{href:"/docs/architecture/configuration",children:"configuration key"})," ",(0,r.jsx)(n.code,{children:"settings.openapi.useHooks"})," is set to ",(0,r.jsx)(n.code,{children:"true"}),", we can get a full documentation of the API using ",(0,r.jsx)(n.a,{href:"/docs/common/openapi-and-swagger-ui",children:"Swagger UI"})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.img,{alt:"Example of documentation",src:t(80958).A+"",width:"2538",height:"1298"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(a,{...e})}):a(e)}},80958:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/rest-openapi-28b755aa0e71f5a2a6ec387ea2c8ec98.png"},28453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>d});var r=t(96540);const s={},i=r.createContext(s);function o(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e2a8b2ab.06dc9224.js b/assets/js/e2a8b2ab.39886df4.js similarity index 53% rename from assets/js/e2a8b2ab.06dc9224.js rename to assets/js/e2a8b2ab.39886df4.js index 2f27f2e93f..739373a0d0 100644 --- a/assets/js/e2a8b2ab.06dc9224.js +++ b/assets/js/e2a8b2ab.39886df4.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1408],{10732:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>d});var r=n(74848),o=n(28453);const s={title:"The User and Story Models",id:"tuto-3-the-models",slug:"3-the-models"},a=void 0,i={id:"tutorials/real-world-example-with-react/tuto-3-the-models",title:"The User and Story Models",description:"Now that the database connection is established, you can create your two entities User and Story.",source:"@site/docs/tutorials/real-world-example-with-react/3-the-models.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/3-the-models",permalink:"/docs/tutorials/real-world-example-with-react/3-the-models",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/3-the-models.md",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"The User and Story Models",id:"tuto-3-the-models",slug:"3-the-models"},sidebar:"someSidebar",previous:{title:"Database Set Up",permalink:"/docs/tutorials/real-world-example-with-react/2-database-set-up"},next:{title:"The Shell Scripts",permalink:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts"}},l={},d=[{value:"The User Model",id:"the-user-model",level:2},{value:"The Story Model",id:"the-story-model",level:2},{value:"Run Migrations",id:"run-migrations",level:2}];function c(e){const t={blockquote:"blockquote",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(t.p,{children:["Now that the database connection is established, you can create your two entities ",(0,r.jsx)(t.code,{children:"User"})," and ",(0,r.jsx)(t.code,{children:"Story"}),"."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"User"})," entity will be the model used by the framework to identify users and the ",(0,r.jsx)(t.code,{children:"Story"})," entity will represent the users' posts."]}),"\n",(0,r.jsxs)(t.h2,{id:"the-user-model",children:["The ",(0,r.jsx)(t.code,{children:"User"})," Model"]}),"\n",(0,r.jsxs)(t.p,{children:["Open the ",(0,r.jsx)(t.code,{children:"user.entity.ts"})," file from the ",(0,r.jsx)(t.code,{children:"entities"})," directory and add four new properties to your model: ",(0,r.jsx)(t.code,{children:"email"}),", ",(0,r.jsx)(t.code,{children:"password"}),", ",(0,r.jsx)(t.code,{children:"name"})," and ",(0,r.jsx)(t.code,{children:"avatar"}),"."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"avatar"})," column will contain the paths to the profile images."]}),"\n",(0,r.jsxs)(t.p,{children:["You will also need to export an additional model ",(0,r.jsx)(t.code,{children:"DatabaseSession"})," from the ",(0,r.jsx)(t.code,{children:"@foal/typeorm"})," package. You don't need to worry about it now, it will be used later in the tutorial when you add authentication."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n\n @Column()\n name: string;\n\n @Column()\n avatar: string;\n\n}\n\n// This line is required. It will be used to create the SQL session table later in the tutorial.\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"the-story-model",children:["The ",(0,r.jsx)(t.code,{children:"Story"})," Model"]}),"\n",(0,r.jsx)(t.p,{children:"Then create your second entity."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"foal generate entity story\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Open the new file and add three new properties: ",(0,r.jsx)(t.code,{children:"author"}),", ",(0,r.jsx)(t.code,{children:"title"})," and ",(0,r.jsx)(t.code,{children:"link"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';\nimport { User } from './user.entity';\n\n@Entity()\nexport class Story extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @ManyToOne(type => User, { nullable: false })\n author: User;\n\n @Column()\n title: string;\n\n @Column()\n link: string;\n\n}\n"})}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsxs)(t.p,{children:["By default, TypeORM allows ",(0,r.jsx)(t.em,{children:"many-to-one"})," relationships to be nullable. The option passed to the decorator specifies that this one cannot be."]}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"run-migrations",children:"Run Migrations"}),"\n",(0,r.jsx)(t.p,{children:"Finally, create the tables in the database. Generate the migrations from the entities and run them."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Three new tables are added to the database: the ",(0,r.jsx)(t.code,{children:"user"})," and ",(0,r.jsx)(t.code,{children:"story"})," tables and a ",(0,r.jsx)(t.code,{children:"sessions"})," table."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"+------------+-----------+-------------------------------------+\n| user |\n+------------+-----------+-------------------------------------+\n| id | integer | PRIMARY KEY AUTO_INCREMENT NOT NULL |\n| email | varchar | UNIQUE NOT NULL |\n| password | varchar | NOT NULL |\n| name | varchar | NOT NULL |\n| avatar | varchar | NOT NULL |\n+------------+-----------+-------------------------------------+\n"})}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"+------------+-----------+-------------------------------------+\n| story |\n+------------+-----------+-------------------------------------+\n| id | integer | PRIMARY KEY AUTO_INCREMENT NOT NULL |\n| authorId | integer | NOT NULL |\n| title | varchar | NOT NULL |\n| link | varchar | NOT NULL |\n+------------+-----------+-------------------------------------+\n"})})]})}function h(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>i});var r=n(96540);const o={},s=r.createContext(o);function a(e){const t=r.useContext(s);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),r.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[1408],{10732:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>d});var r=n(74848),o=n(28453);const s={title:"The User and Story Models",id:"tuto-3-the-models",slug:"3-the-models"},a=void 0,i={id:"tutorials/real-world-example-with-react/tuto-3-the-models",title:"The User and Story Models",description:"Now that the database connection is established, you can create your two entities User and Story.",source:"@site/docs/tutorials/real-world-example-with-react/3-the-models.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/3-the-models",permalink:"/docs/tutorials/real-world-example-with-react/3-the-models",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/3-the-models.md",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"The User and Story Models",id:"tuto-3-the-models",slug:"3-the-models"},sidebar:"someSidebar",previous:{title:"Database Set Up",permalink:"/docs/tutorials/real-world-example-with-react/2-database-set-up"},next:{title:"The Shell Scripts",permalink:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts"}},l={},d=[{value:"The User Model",id:"the-user-model",level:2},{value:"The Story Model",id:"the-story-model",level:2},{value:"Run Migrations",id:"run-migrations",level:2}];function c(e){const t={blockquote:"blockquote",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(t.p,{children:["Now that the database connection is established, you can create your two entities ",(0,r.jsx)(t.code,{children:"User"})," and ",(0,r.jsx)(t.code,{children:"Story"}),"."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"User"})," entity will be the model used by the framework to identify users and the ",(0,r.jsx)(t.code,{children:"Story"})," entity will represent the users' posts."]}),"\n",(0,r.jsxs)(t.h2,{id:"the-user-model",children:["The ",(0,r.jsx)(t.code,{children:"User"})," Model"]}),"\n",(0,r.jsxs)(t.p,{children:["Open the ",(0,r.jsx)(t.code,{children:"user.entity.ts"})," file from the ",(0,r.jsx)(t.code,{children:"entities"})," directory and add four new properties to your model: ",(0,r.jsx)(t.code,{children:"email"}),", ",(0,r.jsx)(t.code,{children:"password"}),", ",(0,r.jsx)(t.code,{children:"name"})," and ",(0,r.jsx)(t.code,{children:"avatar"}),"."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"avatar"})," column will contain the paths to the profile images."]}),"\n",(0,r.jsxs)(t.p,{children:["You will also need to export an additional model ",(0,r.jsx)(t.code,{children:"DatabaseSession"})," from the ",(0,r.jsx)(t.code,{children:"@foal/typeorm"})," package. You don't need to worry about it now, it will be used later in the tutorial when you add authentication."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n\n @Column()\n name: string;\n\n @Column()\n avatar: string;\n\n}\n\n// This line is required. It will be used to create the SQL session table later in the tutorial.\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,r.jsxs)(t.h2,{id:"the-story-model",children:["The ",(0,r.jsx)(t.code,{children:"Story"})," Model"]}),"\n",(0,r.jsx)(t.p,{children:"Then create your second entity."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"npx foal generate entity story\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Open the new file and add three new properties: ",(0,r.jsx)(t.code,{children:"author"}),", ",(0,r.jsx)(t.code,{children:"title"})," and ",(0,r.jsx)(t.code,{children:"link"}),"."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';\nimport { User } from './user.entity';\n\n@Entity()\nexport class Story extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @ManyToOne(type => User, { nullable: false })\n author: User;\n\n @Column()\n title: string;\n\n @Column()\n link: string;\n\n}\n"})}),"\n",(0,r.jsxs)(t.blockquote,{children:["\n",(0,r.jsxs)(t.p,{children:["By default, TypeORM allows ",(0,r.jsx)(t.em,{children:"many-to-one"})," relationships to be nullable. The option passed to the decorator specifies that this one cannot be."]}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"run-migrations",children:"Run Migrations"}),"\n",(0,r.jsx)(t.p,{children:"Finally, create the tables in the database. Generate the migrations from the entities and run them."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,r.jsxs)(t.p,{children:["Three new tables are added to the database: the ",(0,r.jsx)(t.code,{children:"user"})," and ",(0,r.jsx)(t.code,{children:"story"})," tables and a ",(0,r.jsx)(t.code,{children:"sessions"})," table."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"+------------+-----------+-------------------------------------+\n| user |\n+------------+-----------+-------------------------------------+\n| id | integer | PRIMARY KEY AUTO_INCREMENT NOT NULL |\n| email | varchar | UNIQUE NOT NULL |\n| password | varchar | NOT NULL |\n| name | varchar | NOT NULL |\n| avatar | varchar | NOT NULL |\n+------------+-----------+-------------------------------------+\n"})}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{children:"+------------+-----------+-------------------------------------+\n| story |\n+------------+-----------+-------------------------------------+\n| id | integer | PRIMARY KEY AUTO_INCREMENT NOT NULL |\n| authorId | integer | NOT NULL |\n| title | varchar | NOT NULL |\n| link | varchar | NOT NULL |\n+------------+-----------+-------------------------------------+\n"})})]})}function h(e={}){const{wrapper:t}={...(0,o.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},28453:(e,t,n)=>{n.d(t,{R:()=>a,x:()=>i});var r=n(96540);const o={},s=r.createContext(o);function a(e){const t=r.useContext(s);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),r.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e3ec4ccc.ad2f66b3.js b/assets/js/e3ec4ccc.ad2f66b3.js new file mode 100644 index 0000000000..b9bf88a1cb --- /dev/null +++ b/assets/js/e3ec4ccc.ad2f66b3.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3658],{28105:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>c,toc:()=>u});var t=s(74848),r=s(28453),o=s(11470),i=s(19365);const a={title:"Quick Start"},l=void 0,c={id:"authentication/quick-start",title:"Quick Start",description:"Authentication is the process of verifying that a user is who he or she claims to be. It answers the question Who is the user?.",source:"@site/docs/authentication/quick-start.md",sourceDirName:"authentication",slug:"/authentication/quick-start",permalink:"/docs/authentication/quick-start",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/quick-start.md",tags:[],version:"current",frontMatter:{title:"Quick Start"},sidebar:"someSidebar",previous:{title:"Prisma",permalink:"/docs/databases/other-orm/prisma"},next:{title:"Users",permalink:"/docs/authentication/user-class"}},d={},u=[{value:"The Basics",id:"the-basics",level:2},{value:"The Available Tokens (step 1)",id:"the-available-tokens-step-1",level:3},{value:"The Authentication Hooks (step 2)",id:"the-authentication-hooks-step-2",level:3},{value:"Examples",id:"examples",level:2},{value:"SPA, 3rd party APIs, Mobile (cookies)",id:"spa-3rd-party-apis-mobile-cookies",level:3},{value:"Using Session Tokens",id:"using-session-tokens",level:4},{value:"Using JSON Web Tokens",id:"using-json-web-tokens",level:4},{value:"SPA, 3rd party APIs, Mobile (Authorization header)",id:"spa-3rd-party-apis-mobile-authorization-header",level:3},{value:"Using Session Tokens",id:"using-session-tokens-1",level:4},{value:"Using JSON Web Tokens",id:"using-json-web-tokens-1",level:4},{value:"SSR Applications (cookies)",id:"ssr-applications-cookies",level:3},{value:"Using Session Tokens",id:"using-session-tokens-2",level:4}];function p(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Authentication"})," is the process of verifying that a user is who he or she claims to be. It answers the question ",(0,t.jsx)(n.em,{children:"Who is the user?"}),"."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Example: a user enters their login credentials to connect to the application"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Authorization"}),", also known as ",(0,t.jsx)(n.em,{children:"Access Control"}),", is the process of determining what an authenticated user is allowed to do. It answers the question ",(0,t.jsx)(n.em,{children:"Does the user has the right to do what they ask?"}),"."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Example: a user tries to access the administrator page"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"This document focuses on explaining how authentication works in FoalTS and gives several code examples to get started quickly. Further explanations are given in other pages of the documentation."}),"\n",(0,t.jsx)(n.h2,{id:"the-basics",children:"The Basics"}),"\n",(0,t.jsx)(n.p,{children:"The strength of FoalTS authentication system is that it can be used in a wide variety of applications. Whether you want to build a stateless REST API that uses social ID tokens or a traditional web application with templates, cookies and redirects, FoalTS provides you with the tools to do so. You can choose the elements you need and build your own authentication process."}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Auth Support"}),(0,t.jsx)(n.th,{})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Kind of Application"}),(0,t.jsx)(n.td,{children:"API, Regular Web App, SPA+API, Mobile+API"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"State management"}),(0,t.jsx)(n.td,{children:"Stateful (Session Tokens), Stateless (JSON Web Tokens)\xa0"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Credentials"}),(0,t.jsx)(n.td,{children:"Passwords, Social\xa0"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Token storage"}),(0,t.jsx)(n.td,{children:"Cookies, localStorage, Mobile, etc"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"Whatever architecture you choose, the authentication process will always follow the same pattern."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Step 1: the user logs in."})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"In some architectures, this step might be delegated to an external service: Google, Cognito, Auth0, etc"})}),"\n"]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Verify the credentials (email & password, username & password, social, etc)."}),"\n",(0,t.jsx)(n.li,{children:"Generate a token (stateless or stateful)."}),"\n",(0,t.jsx)(n.li,{children:"Return the token to the client (in a cookie, in the response body or in a header)."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Step 2: once logged in, the user keeps being authenticated on subsequent requests."})}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"On each request, receive and check the token and retrieve the associated user if the token is valid."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Authentication architecture",src:s(16316).A+"",width:"522",height:"370"})}),"\n",(0,t.jsx)(n.h3,{id:"the-available-tokens-step-1",children:"The Available Tokens (step 1)"}),"\n",(0,t.jsx)(n.p,{children:"FoalTS provides two ways to generate tokens:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Session Tokens"})," (stateful): They are probably the easiest way to manage authentication with Foal. Creation is straightforward, expiration is managed automatically and revocation is easy. Using session tokens keeps your code concise and does not require additional knowledge."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"Unlike other restrictive session management systems, FoalTS sessions are not limited to traditional applications that use cookies, redirection and server-side rendering. You can choose to use sessions without cookies, in a SPA+API or Mobile+API architecture and deploy your application to a serverless environment."}),"\n"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"JSON Web Tokens"})," (stateless): For more advanced developers, JWTs can be used to create stateless authentication or authentication that works with external social providers."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"the-authentication-hooks-step-2",children:"The Authentication Hooks (step 2)"}),"\n",(0,t.jsxs)(n.p,{children:["In step 2, the hook ",(0,t.jsx)(n.code,{children:"@UseSessions"})," takes care of checking the session tokens and retrieve their associated user. The same applies to ",(0,t.jsx)(n.code,{children:"JWTRequired"})," and ",(0,t.jsx)(n.code,{children:"JWTOptional"})," with JSON Web Tokens."]}),"\n",(0,t.jsx)(n.p,{children:"You will find more information in the documentation pages dedicated to them."}),"\n",(0,t.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,t.jsx)(n.p,{children:"The examples below can be used directly in your application to configure login, logout and signup routes. You can use them as they are or customize them to meet your specific needs."}),"\n",(0,t.jsxs)(n.p,{children:["For these examples, we will use TypeORM as default ORM and emails and passwords as credentials. An API will allow authenticated users to list ",(0,t.jsx)(n.em,{children:"products"})," with the request ",(0,t.jsx)(n.code,{children:"GET /api/products"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["The definition of the ",(0,t.jsx)(n.code,{children:"User"})," entity is common to all the examples and is as follows:"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/entities/user.entity.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n}\n\n// Exporting this line is required\n// when using session tokens with TypeORM.\n// It will be used by `npm run makemigrations`\n// to generate the SQL session table.\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,t.jsx)(n.h3,{id:"spa-3rd-party-apis-mobile-cookies",children:"SPA, 3rd party APIs, Mobile (cookies)"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["As you use cookies, you must add a ",(0,t.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF protection"})," to your application."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In these implementations, the client does not have to handle the receipt, sending and expiration of the tokens itself. All is handled transparently by the server using cookies."}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note: If you develop a native Mobile application, you may need to enable a ",(0,t.jsx)(n.em,{children:"cookie"})," plugin in your code."]})}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note: If your server and client do not have the same origins, you may also need to enable ",(0,t.jsx)(n.a,{href:"/docs/security/cors",children:"CORS requests"}),"."]})}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"using-session-tokens",children:"Using Session Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["First, make sure that the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity is exported in ",(0,t.jsx)(n.code,{children:"user.entity.ts"}),". Then build and run the migrations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, dependency, IAppController, Store, UseSessions } from '@foal/core';\n\nimport { User } from './entities';\nimport { ApiController, AuthController } from './controllers';\n\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class AppController implements IAppController {\n // This line is required.\n @dependency\n store: Store;\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseOK();\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseOK();\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n await ctx.session!.destroy();\n\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UserRequired } from '@foal/core';\n\n@UserRequired()\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"using-json-web-tokens",children:"Using JSON Web Tokens"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"When using stateless authentication with JWT, you must manage the renewal of tokens after their expiration yourself. You also cannot list all users logged into your application."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"First, generate a base64-encoded secret."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npx foal createsecret\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Save this secret in a ",(0,t.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:'JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n'})}),"\n",(0,t.jsx)(n.p,{children:"Update your configuration to retrieve the secret."}),"\n",(0,t.jsxs)(o.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n secret: env(JWT_SECRET)\n secretEncoding: base64\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "env(JWT_SECRET)",\n "secretEncoding": "base64"\n }\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'const { Env } = require(\'@foal/core\');\n\nmodule.exports = {\n settings: {\n jwt: {\n secret: Env.get("JWT_SECRET"),\n secretEncoding: "base64"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\n\nexport class AppController implements IAppController {\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\nimport { getSecretOrPrivateKey, removeAuthCookie, setAuthCookie } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\nimport { promisify } from 'util';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n const response = new HttpResponseOK();\n await setAuthCookie(response, await this.createJWT(user));\n return response;\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n const response = new HttpResponseOK();\n await setAuthCookie(response, await this.createJWT(user));\n return response;\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n const response = new HttpResponseOK();\n removeAuthCookie(response);\n return response;\n }\n\n private async createJWT(user: User): Promise {\n const payload = {\n email: user.email,\n id: user.id,\n };\n \n return promisify(sign as any)(\n payload,\n getSecretOrPrivateKey(),\n { subject: user.id.toString() }\n );\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nimport { User } from './entities';\n\n@JWTRequired({\n cookie: true,\n // Add the line below if you prefer ctx.user\n // to be an instance of User instead of the JWT payload.\n // user: (id: number) => User.findOneBy({ id })\n})\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"spa-3rd-party-apis-mobile-authorization-header",children:"SPA, 3rd party APIs, Mobile (Authorization header)"}),"\n",(0,t.jsxs)(n.p,{children:["In these implementations, the user logs in with the route ",(0,t.jsx)(n.code,{children:"POST /auth/login"})," and receives a token in exchange in the response body. Then, when the client makes a request to the API, the token must be included in the ",(0,t.jsx)(n.code,{children:"Authorization"})," header using the bearer sheme."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Authorization: Bearer my-token\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note: If your server and client do not have the same origins, you may also need to enable ",(0,t.jsx)(n.a,{href:"/docs/security/cors",children:"CORS requests"}),"."]})}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"using-session-tokens-1",children:"Using Session Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["First, make sure that the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity is exported in ",(0,t.jsx)(n.code,{children:"user.entity.ts"}),". Then build and run the migrations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\n\nexport class AppController implements IAppController {\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, Store, UseSessions, ValidateBody, verifyPassword } from '@foal/core';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\n@UseSessions()\nexport class AuthController {\n @dependency\n store: Store;\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n ctx.session = await createSession(this.store);\n ctx.session.setUser(user);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n ctx.session = await createSession(this.store);\n ctx.session.setUser(user);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n if (ctx.session) {\n await ctx.session.destroy();\n }\n\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UserRequired, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n// The `request` option returns a pretty message if the Authorization header is not here.\n@UseSessions({\n required: true,\n user: (id: number) => User.findOneBy({ id }),\n})\n@UserRequired()\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"using-json-web-tokens-1",children:"Using JSON Web Tokens"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"When using stateless authentication with JWT, you must manage the renewal of tokens after their expiration yourself. You also cannot list all users logged into your application."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"First, generate a base64-encoded secret."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npx foal createsecret\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Save this secret in a ",(0,t.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:'JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n'})}),"\n",(0,t.jsx)(n.p,{children:"Update your configuration to retrieve the secret."}),"\n",(0,t.jsxs)(o.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n secret: env(JWT_SECRET)\n secretEncoding: base64\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "env(JWT_SECRET)",\n "secretEncoding": "base64"\n }\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'const { Env } = require(\'@foal/core\');\n\nmodule.exports = {\n settings: {\n jwt: {\n secret: Env.get("JWT_SECRET"),\n secretEncoding: "base64"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\n\nexport class AppController implements IAppController {\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\nimport { promisify } from 'util';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n return new HttpResponseOK({\n token: await this.createJWT(user)\n });\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n return new HttpResponseOK({\n token: await this.createJWT(user)\n });\n }\n\n private async createJWT(user: User): Promise {\n const payload = {\n email: user.email,\n id: user.id,\n };\n \n return promisify(sign as any)(\n payload,\n getSecretOrPrivateKey(),\n { subject: user.id.toString() }\n );\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nimport { User } from '../entities';\n\n@JWTRequired({\n // Add the line below if you prefer ctx.user\n // to be an instance of User instead of the JWT payload.\n // user: (id: number) => User.findOneBy({ id })\n})\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"ssr-applications-cookies",children:"SSR Applications (cookies)"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["As you use cookies, you must add a ",(0,t.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF protection"})," to your application."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In this implementation, the client does not have to handle the receipt, sending and expiration of the tokens itself. All is handled transparently by the server using cookies and redirections."}),"\n",(0,t.jsx)(n.h4,{id:"using-session-tokens-2",children:"Using Session Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["First, make sure that the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity is exported in ",(0,t.jsx)(n.code,{children:"user.entity.ts"}),". Then build and run the migrations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, controller, dependency, Get, IAppController, render, Store, UserRequired, UseSessions } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\nimport { User } from './entities';\n\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class AppController implements IAppController {\n // This line is required.\n @dependency\n store: Store;\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n @Get('/')\n @UserRequired({ redirectTo: '/login' })\n index() {\n return render('./templates/index.html');\n }\n\n @Get('/login')\n login(ctx: Context) {\n return render('./templates/login.html', {\n errorMessage: ctx.session!.get('errorMessage', '')\n });\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseRedirect, Post, ValidateBody, verifyPassword } from '@foal/core';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseRedirect('/');\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n ctx.session!.set('errorMessage', 'Unknown email.', { flash: true });\n return new HttpResponseRedirect('/login');\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n ctx.session!.set('errorMessage', 'Invalid password.', { flash: true });\n return new HttpResponseRedirect('/login');\n }\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseRedirect('/');\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n await ctx.session!.destroy();\n\n return new HttpResponseRedirect('/login');\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UserRequired } from '@foal/core';\n\n@UserRequired()\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"templates/login.html"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-html",children:'\n\n\n \n \n Login\n\n\n {{ errorMessage }}\n
\n \n \n \n
\n\n\n'})})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(p,{...e})}):p(e)}},19365:(e,n,s)=>{s.d(n,{A:()=>i});s(96540);var t=s(34164);const r={tabItem:"tabItem_Ymn6"};var o=s(74848);function i(e){let{children:n,hidden:s,className:i}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,t.A)(r.tabItem,i),hidden:s,children:n})}},11470:(e,n,s)=>{s.d(n,{A:()=>v});var t=s(96540),r=s(34164),o=s(23104),i=s(56347),a=s(205),l=s(57485),c=s(31682),d=s(89466);function u(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:s}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:s,attributes:t,default:r}}=e;return{value:n,label:s,attributes:t,default:r}}))}(s);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,s])}function h(e){let{value:n,tabValues:s}=e;return s.some((e=>e.value===n))}function m(e){let{queryString:n=!1,groupId:s}=e;const r=(0,i.W6)(),o=function(e){let{queryString:n=!1,groupId:s}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!s)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return s??null}({queryString:n,groupId:s});return[(0,l.aZ)(o),(0,t.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(r.location.search);n.set(o,e),r.replace({...r.location,search:n.toString()})}),[o,r])]}function x(e){const{defaultValue:n,queryString:s=!1,groupId:r}=e,o=p(e),[i,l]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:s}=e;if(0===s.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!h({value:n,tabValues:s}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${s.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=s.find((e=>e.default))??s[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:o}))),[c,u]=m({queryString:s,groupId:r}),[x,g]=function(e){let{groupId:n}=e;const s=function(e){return e?`docusaurus.tab.${e}`:null}(n),[r,o]=(0,d.Dv)(s);return[r,(0,t.useCallback)((e=>{s&&o.set(e)}),[s,o])]}({groupId:r}),j=(()=>{const e=c??x;return h({value:e,tabValues:o})?e:null})();(0,a.A)((()=>{j&&l(j)}),[j]);return{selectedValue:i,selectValue:(0,t.useCallback)((e=>{if(!h({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),g(e)}),[u,g,o]),tabValues:o}}var g=s(92303);const j={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var y=s(74848);function f(e){let{className:n,block:s,selectedValue:t,selectValue:i,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,o.a_)(),d=e=>{const n=e.currentTarget,s=l.indexOf(n),r=a[s].value;r!==t&&(c(n),i(r))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const s=l.indexOf(e.currentTarget)+1;n=l[s]??l[0];break}case"ArrowLeft":{const s=l.indexOf(e.currentTarget)-1;n=l[s]??l[l.length-1];break}}n?.focus()};return(0,y.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":s},n),children:a.map((e=>{let{value:n,label:s,attributes:o}=e;return(0,y.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>l.push(e),onKeyDown:u,onClick:d,...o,className:(0,r.A)("tabs__item",j.tabItem,o?.className,{"tabs__item--active":t===n}),children:s??n},n)}))})}function b(e){let{lazy:n,children:s,selectedValue:r}=e;const o=(Array.isArray(s)?s:[s]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===r));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,y.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==r})))})}function w(e){const n=x(e);return(0,y.jsxs)("div",{className:(0,r.A)("tabs-container",j.tabList),children:[(0,y.jsx)(f,{...e,...n}),(0,y.jsx)(b,{...e,...n})]})}function v(e){const n=(0,g.A)();return(0,y.jsx)(w,{...e,children:u(e.children)},String(n))}},16316:(e,n,s)=>{s.d(n,{A:()=>t});const t=s.p+"assets/images/auth-architecture-b33065fc731227be200c1fb1a412bf37.png"},28453:(e,n,s)=>{s.d(n,{R:()=>i,x:()=>a});var t=s(96540);const r={},o=t.createContext(r);function i(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e3ec4ccc.d7130146.js b/assets/js/e3ec4ccc.d7130146.js deleted file mode 100644 index 5ed2f67637..0000000000 --- a/assets/js/e3ec4ccc.d7130146.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3658],{28105:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>c,toc:()=>u});var t=s(74848),r=s(28453),o=s(11470),i=s(19365);const a={title:"Quick Start"},l=void 0,c={id:"authentication/quick-start",title:"Quick Start",description:"Authentication is the process of verifying that a user is who he or she claims to be. It answers the question Who is the user?.",source:"@site/docs/authentication/quick-start.md",sourceDirName:"authentication",slug:"/authentication/quick-start",permalink:"/docs/authentication/quick-start",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/authentication/quick-start.md",tags:[],version:"current",frontMatter:{title:"Quick Start"},sidebar:"someSidebar",previous:{title:"Prisma",permalink:"/docs/databases/other-orm/prisma"},next:{title:"Users",permalink:"/docs/authentication/user-class"}},d={},u=[{value:"The Basics",id:"the-basics",level:2},{value:"The Available Tokens (step 1)",id:"the-available-tokens-step-1",level:3},{value:"The Authentication Hooks (step 2)",id:"the-authentication-hooks-step-2",level:3},{value:"Examples",id:"examples",level:2},{value:"SPA, 3rd party APIs, Mobile (cookies)",id:"spa-3rd-party-apis-mobile-cookies",level:3},{value:"Using Session Tokens",id:"using-session-tokens",level:4},{value:"Using JSON Web Tokens",id:"using-json-web-tokens",level:4},{value:"SPA, 3rd party APIs, Mobile (Authorization header)",id:"spa-3rd-party-apis-mobile-authorization-header",level:3},{value:"Using Session Tokens",id:"using-session-tokens-1",level:4},{value:"Using JSON Web Tokens",id:"using-json-web-tokens-1",level:4},{value:"SSR Applications (cookies)",id:"ssr-applications-cookies",level:3},{value:"Using Session Tokens",id:"using-session-tokens-2",level:4}];function p(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Authentication"})," is the process of verifying that a user is who he or she claims to be. It answers the question ",(0,t.jsx)(n.em,{children:"Who is the user?"}),"."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Example: a user enters their login credentials to connect to the application"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Authorization"}),", also known as ",(0,t.jsx)(n.em,{children:"Access Control"}),", is the process of determining what an authenticated user is allowed to do. It answers the question ",(0,t.jsx)(n.em,{children:"Does the user has the right to do what they ask?"}),"."]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.em,{children:"Example: a user tries to access the administrator page"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"This document focuses on explaining how authentication works in FoalTS and gives several code examples to get started quickly. Further explanations are given in other pages of the documentation."}),"\n",(0,t.jsx)(n.h2,{id:"the-basics",children:"The Basics"}),"\n",(0,t.jsx)(n.p,{children:"The strength of FoalTS authentication system is that it can be used in a wide variety of applications. Whether you want to build a stateless REST API that uses social ID tokens or a traditional web application with templates, cookies and redirects, FoalTS provides you with the tools to do so. You can choose the elements you need and build your own authentication process."}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Auth Support"}),(0,t.jsx)(n.th,{})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Kind of Application"}),(0,t.jsx)(n.td,{children:"API, Regular Web App, SPA+API, Mobile+API"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"State management"}),(0,t.jsx)(n.td,{children:"Stateful (Session Tokens), Stateless (JSON Web Tokens)\xa0"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Credentials"}),(0,t.jsx)(n.td,{children:"Passwords, Social\xa0"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:"Token storage"}),(0,t.jsx)(n.td,{children:"Cookies, localStorage, Mobile, etc"})]})]})]}),"\n",(0,t.jsx)(n.p,{children:"Whatever architecture you choose, the authentication process will always follow the same pattern."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Step 1: the user logs in."})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"In some architectures, this step might be delegated to an external service: Google, Cognito, Auth0, etc"})}),"\n"]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Verify the credentials (email & password, username & password, social, etc)."}),"\n",(0,t.jsx)(n.li,{children:"Generate a token (stateless or stateful)."}),"\n",(0,t.jsx)(n.li,{children:"Return the token to the client (in a cookie, in the response body or in a header)."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Step 2: once logged in, the user keeps being authenticated on subsequent requests."})}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"On each request, receive and check the token and retrieve the associated user if the token is valid."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"Authentication architecture",src:s(16316).A+"",width:"522",height:"370"})}),"\n",(0,t.jsx)(n.h3,{id:"the-available-tokens-step-1",children:"The Available Tokens (step 1)"}),"\n",(0,t.jsx)(n.p,{children:"FoalTS provides two ways to generate tokens:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Session Tokens"})," (stateful): They are probably the easiest way to manage authentication with Foal. Creation is straightforward, expiration is managed automatically and revocation is easy. Using session tokens keeps your code concise and does not require additional knowledge."]}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"Unlike other restrictive session management systems, FoalTS sessions are not limited to traditional applications that use cookies, redirection and server-side rendering. You can choose to use sessions without cookies, in a SPA+API or Mobile+API architecture and deploy your application to a serverless environment."}),"\n"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"JSON Web Tokens"})," (stateless): For more advanced developers, JWTs can be used to create stateless authentication or authentication that works with external social providers."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"the-authentication-hooks-step-2",children:"The Authentication Hooks (step 2)"}),"\n",(0,t.jsxs)(n.p,{children:["In step 2, the hook ",(0,t.jsx)(n.code,{children:"@UseSessions"})," takes care of checking the session tokens and retrieve their associated user. The same applies to ",(0,t.jsx)(n.code,{children:"JWTRequired"})," and ",(0,t.jsx)(n.code,{children:"JWTOptional"})," with JSON Web Tokens."]}),"\n",(0,t.jsx)(n.p,{children:"You will find more information in the documentation pages dedicated to them."}),"\n",(0,t.jsx)(n.h2,{id:"examples",children:"Examples"}),"\n",(0,t.jsx)(n.p,{children:"The examples below can be used directly in your application to configure login, logout and signup routes. You can use them as they are or customize them to meet your specific needs."}),"\n",(0,t.jsxs)(n.p,{children:["For these examples, we will use TypeORM as default ORM and emails and passwords as credentials. An API will allow authenticated users to list ",(0,t.jsx)(n.em,{children:"products"})," with the request ",(0,t.jsx)(n.code,{children:"GET /api/products"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["The definition of the ",(0,t.jsx)(n.code,{children:"User"})," entity is common to all the examples and is as follows:"]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/entities/user.entity.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column()\n password: string;\n}\n\n// Exporting this line is required\n// when using session tokens with TypeORM.\n// It will be used by `npm run makemigrations`\n// to generate the SQL session table.\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,t.jsx)(n.h3,{id:"spa-3rd-party-apis-mobile-cookies",children:"SPA, 3rd party APIs, Mobile (cookies)"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["As you use cookies, you must add a ",(0,t.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF protection"})," to your application."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In these implementations, the client does not have to handle the receipt, sending and expiration of the tokens itself. All is handled transparently by the server using cookies."}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note: If you develop a native Mobile application, you may need to enable a ",(0,t.jsx)(n.em,{children:"cookie"})," plugin in your code."]})}),"\n"]}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note: If your server and client do not have the same origins, you may also need to enable ",(0,t.jsx)(n.a,{href:"/docs/security/cors",children:"CORS requests"}),"."]})}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"using-session-tokens",children:"Using Session Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["First, make sure that the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity is exported in ",(0,t.jsx)(n.code,{children:"user.entity.ts"}),". Then build and run the migrations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, dependency, IAppController, Store, UseSessions } from '@foal/core';\n\nimport { User } from './entities';\nimport { ApiController, AuthController } from './controllers';\n\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class AppController implements IAppController {\n // This line is required.\n @dependency\n store: Store;\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseOK();\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseOK();\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n await ctx.session!.destroy();\n\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UserRequired } from '@foal/core';\n\n@UserRequired()\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"using-json-web-tokens",children:"Using JSON Web Tokens"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"When using stateless authentication with JWT, you must manage the renewal of tokens after their expiration yourself. You also cannot list all users logged into your application."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"First, generate a base64-encoded secret."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"foal createsecret\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Save this secret in a ",(0,t.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:'JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n'})}),"\n",(0,t.jsx)(n.p,{children:"Update your configuration to retrieve the secret."}),"\n",(0,t.jsxs)(o.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n secret: env(JWT_SECRET)\n secretEncoding: base64\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "env(JWT_SECRET)",\n "secretEncoding": "base64"\n }\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'const { Env } = require(\'@foal/core\');\n\nmodule.exports = {\n settings: {\n jwt: {\n secret: Env.get("JWT_SECRET"),\n secretEncoding: "base64"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\n\nexport class AppController implements IAppController {\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\nimport { getSecretOrPrivateKey, removeAuthCookie, setAuthCookie } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\nimport { promisify } from 'util';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n const response = new HttpResponseOK();\n await setAuthCookie(response, await this.createJWT(user));\n return response;\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n const response = new HttpResponseOK();\n await setAuthCookie(response, await this.createJWT(user));\n return response;\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n const response = new HttpResponseOK();\n removeAuthCookie(response);\n return response;\n }\n\n private async createJWT(user: User): Promise {\n const payload = {\n email: user.email,\n id: user.id,\n };\n \n return promisify(sign as any)(\n payload,\n getSecretOrPrivateKey(),\n { subject: user.id.toString() }\n );\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nimport { User } from './entities';\n\n@JWTRequired({\n cookie: true,\n // Add the line below if you prefer ctx.user\n // to be an instance of User instead of the JWT payload.\n // user: (id: number) => User.findOneBy({ id })\n})\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"spa-3rd-party-apis-mobile-authorization-header",children:"SPA, 3rd party APIs, Mobile (Authorization header)"}),"\n",(0,t.jsxs)(n.p,{children:["In these implementations, the user logs in with the route ",(0,t.jsx)(n.code,{children:"POST /auth/login"})," and receives a token in exchange in the response body. Then, when the client makes a request to the API, the token must be included in the ",(0,t.jsx)(n.code,{children:"Authorization"})," header using the bearer sheme."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Authorization: Bearer my-token\n"})}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:(0,t.jsxs)(n.em,{children:["Note: If your server and client do not have the same origins, you may also need to enable ",(0,t.jsx)(n.a,{href:"/docs/security/cors",children:"CORS requests"}),"."]})}),"\n"]}),"\n",(0,t.jsx)(n.h4,{id:"using-session-tokens-1",children:"Using Session Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["First, make sure that the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity is exported in ",(0,t.jsx)(n.code,{children:"user.entity.ts"}),". Then build and run the migrations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\n\nexport class AppController implements IAppController {\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, createSession, dependency, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, Store, UseSessions, ValidateBody, verifyPassword } from '@foal/core';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\n@UseSessions()\nexport class AuthController {\n @dependency\n store: Store;\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n ctx.session = await createSession(this.store);\n ctx.session.setUser(user);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n ctx.session = await createSession(this.store);\n ctx.session.setUser(user);\n\n return new HttpResponseOK({\n token: ctx.session.getToken()\n });\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n if (ctx.session) {\n await ctx.session.destroy();\n }\n\n return new HttpResponseOK();\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UserRequired, UseSessions } from '@foal/core';\n\nimport { User } from '../entities';\n\n// The `request` option returns a pretty message if the Authorization header is not here.\n@UseSessions({\n required: true,\n user: (id: number) => User.findOneBy({ id }),\n})\n@UserRequired()\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h4,{id:"using-json-web-tokens-1",children:"Using JSON Web Tokens"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsx)(n.p,{children:"When using stateless authentication with JWT, you must manage the renewal of tokens after their expiration yourself. You also cannot list all users logged into your application."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"First, generate a base64-encoded secret."}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"foal createsecret\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Save this secret in a ",(0,t.jsx)(n.code,{children:".env"})," file."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:'JWT_SECRET="Ak0WcVcGuOoFuZ4oqF1tgqbW6dIAeSacIN6h7qEyJM8="\n'})}),"\n",(0,t.jsx)(n.p,{children:"Update your configuration to retrieve the secret."}),"\n",(0,t.jsxs)(o.A,{defaultValue:"yaml",values:[{label:"YAML",value:"yaml"},{label:"JSON",value:"json"},{label:"JS",value:"js"}],children:[(0,t.jsx)(i.A,{value:"yaml",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-yaml",children:"settings:\n jwt:\n secret: env(JWT_SECRET)\n secretEncoding: base64\n"})})}),(0,t.jsx)(i.A,{value:"json",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-json",children:'{\n "settings": {\n "jwt": {\n "secret": "env(JWT_SECRET)",\n "secretEncoding": "base64"\n }\n }\n}\n'})})}),(0,t.jsx)(i.A,{value:"js",children:(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-javascript",children:'const { Env } = require(\'@foal/core\');\n\nmodule.exports = {\n settings: {\n jwt: {\n secret: Env.get("JWT_SECRET"),\n secretEncoding: "base64"\n }\n }\n}\n'})})})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { controller, IAppController } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\n\nexport class AppController implements IAppController {\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseOK, HttpResponseUnauthorized, Post, ValidateBody, verifyPassword } from '@foal/core';\nimport { getSecretOrPrivateKey } from '@foal/jwt';\nimport { sign } from 'jsonwebtoken';\nimport { promisify } from 'util';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n return new HttpResponseOK({\n token: await this.createJWT(user)\n });\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n return new HttpResponseUnauthorized();\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n return new HttpResponseUnauthorized();\n }\n\n return new HttpResponseOK({\n token: await this.createJWT(user)\n });\n }\n\n private async createJWT(user: User): Promise {\n const payload = {\n email: user.email,\n id: user.id,\n };\n \n return promisify(sign as any)(\n payload,\n getSecretOrPrivateKey(),\n { subject: user.id.toString() }\n );\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK } from '@foal/core';\nimport { JWTRequired } from '@foal/jwt';\n\nimport { User } from '../entities';\n\n@JWTRequired({\n // Add the line below if you prefer ctx.user\n // to be an instance of User instead of the JWT payload.\n // user: (id: number) => User.findOneBy({ id })\n})\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"ssr-applications-cookies",children:"SSR Applications (cookies)"}),"\n",(0,t.jsxs)(n.blockquote,{children:["\n",(0,t.jsxs)(n.p,{children:["As you use cookies, you must add a ",(0,t.jsx)(n.a,{href:"/docs/security/csrf-protection",children:"CSRF protection"})," to your application."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"In this implementation, the client does not have to handle the receipt, sending and expiration of the tokens itself. All is handled transparently by the server using cookies and redirections."}),"\n",(0,t.jsx)(n.h4,{id:"using-session-tokens-2",children:"Using Session Tokens"}),"\n",(0,t.jsxs)(n.p,{children:["First, make sure that the ",(0,t.jsx)(n.code,{children:"DatabaseSession"})," entity is exported in ",(0,t.jsx)(n.code,{children:"user.entity.ts"}),". Then build and run the migrations."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/app.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, controller, dependency, Get, IAppController, render, Store, UserRequired, UseSessions } from '@foal/core';\n\nimport { ApiController, AuthController } from './controllers';\nimport { User } from './entities';\n\n@UseSessions({\n cookie: true,\n user: (id: number) => User.findOneBy({ id }),\n})\nexport class AppController implements IAppController {\n // This line is required.\n @dependency\n store: Store;\n\n subControllers = [\n controller('/auth', AuthController),\n controller('/api', ApiController),\n ];\n\n @Get('/')\n @UserRequired({ redirectTo: '/login' })\n index() {\n return render('./templates/index.html');\n }\n\n @Get('/login')\n login(ctx: Context) {\n return render('./templates/login.html', {\n errorMessage: ctx.session!.get('errorMessage', '')\n });\n }\n\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/auth.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Context, hashPassword, HttpResponseRedirect, Post, ValidateBody, verifyPassword } from '@foal/core';\n\nimport { User } from '../entities';\n\nconst credentialsSchema = {\n additionalProperties: false,\n properties: {\n email: { type: 'string', format: 'email' },\n password: { type: 'string' }\n },\n required: [ 'email', 'password' ],\n type: 'object',\n};\n\nexport class AuthController {\n\n @Post('/signup')\n @ValidateBody(credentialsSchema)\n async signup(ctx: Context) {\n const user = new User();\n user.email = ctx.request.body.email;\n user.password = await hashPassword(ctx.request.body.password);\n await user.save();\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseRedirect('/');\n }\n\n @Post('/login')\n @ValidateBody(credentialsSchema)\n async login(ctx: Context) {\n const user = await User.findOneBy({ email: ctx.request.body.email });\n\n if (!user) {\n ctx.session!.set('errorMessage', 'Unknown email.', { flash: true });\n return new HttpResponseRedirect('/login');\n }\n\n if (!await verifyPassword(ctx.request.body.password, user.password)) {\n ctx.session!.set('errorMessage', 'Invalid password.', { flash: true });\n return new HttpResponseRedirect('/login');\n }\n\n ctx.session!.setUser(user);\n await ctx.session!.regenerateID();\n\n return new HttpResponseRedirect('/');\n }\n\n @Post('/logout')\n async logout(ctx: Context) {\n await ctx.session!.destroy();\n\n return new HttpResponseRedirect('/login');\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"src/app/controllers/api.controller.ts"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-typescript",children:"import { Get, HttpResponseOK, UserRequired } from '@foal/core';\n\n@UserRequired()\nexport class ApiController {\n @Get('/products')\n readProducts() {\n return new HttpResponseOK([]);\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:"templates/login.html"})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-html",children:'\n\n\n \n \n Login\n\n\n {{ errorMessage }}\n
\n \n \n \n
\n\n\n'})})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(p,{...e})}):p(e)}},19365:(e,n,s)=>{s.d(n,{A:()=>i});s(96540);var t=s(34164);const r={tabItem:"tabItem_Ymn6"};var o=s(74848);function i(e){let{children:n,hidden:s,className:i}=e;return(0,o.jsx)("div",{role:"tabpanel",className:(0,t.A)(r.tabItem,i),hidden:s,children:n})}},11470:(e,n,s)=>{s.d(n,{A:()=>v});var t=s(96540),r=s(34164),o=s(23104),i=s(56347),a=s(205),l=s(57485),c=s(31682),d=s(89466);function u(e){return t.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function p(e){const{values:n,children:s}=e;return(0,t.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:s,attributes:t,default:r}}=e;return{value:n,label:s,attributes:t,default:r}}))}(s);return function(e){const n=(0,c.X)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,s])}function h(e){let{value:n,tabValues:s}=e;return s.some((e=>e.value===n))}function m(e){let{queryString:n=!1,groupId:s}=e;const r=(0,i.W6)(),o=function(e){let{queryString:n=!1,groupId:s}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!s)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return s??null}({queryString:n,groupId:s});return[(0,l.aZ)(o),(0,t.useCallback)((e=>{if(!o)return;const n=new URLSearchParams(r.location.search);n.set(o,e),r.replace({...r.location,search:n.toString()})}),[o,r])]}function x(e){const{defaultValue:n,queryString:s=!1,groupId:r}=e,o=p(e),[i,l]=(0,t.useState)((()=>function(e){let{defaultValue:n,tabValues:s}=e;if(0===s.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!h({value:n,tabValues:s}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${s.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const t=s.find((e=>e.default))??s[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:o}))),[c,u]=m({queryString:s,groupId:r}),[x,g]=function(e){let{groupId:n}=e;const s=function(e){return e?`docusaurus.tab.${e}`:null}(n),[r,o]=(0,d.Dv)(s);return[r,(0,t.useCallback)((e=>{s&&o.set(e)}),[s,o])]}({groupId:r}),j=(()=>{const e=c??x;return h({value:e,tabValues:o})?e:null})();(0,a.A)((()=>{j&&l(j)}),[j]);return{selectedValue:i,selectValue:(0,t.useCallback)((e=>{if(!h({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),g(e)}),[u,g,o]),tabValues:o}}var g=s(92303);const j={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var y=s(74848);function f(e){let{className:n,block:s,selectedValue:t,selectValue:i,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,o.a_)(),d=e=>{const n=e.currentTarget,s=l.indexOf(n),r=a[s].value;r!==t&&(c(n),i(r))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const s=l.indexOf(e.currentTarget)+1;n=l[s]??l[0];break}case"ArrowLeft":{const s=l.indexOf(e.currentTarget)-1;n=l[s]??l[l.length-1];break}}n?.focus()};return(0,y.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":s},n),children:a.map((e=>{let{value:n,label:s,attributes:o}=e;return(0,y.jsx)("li",{role:"tab",tabIndex:t===n?0:-1,"aria-selected":t===n,ref:e=>l.push(e),onKeyDown:u,onClick:d,...o,className:(0,r.A)("tabs__item",j.tabItem,o?.className,{"tabs__item--active":t===n}),children:s??n},n)}))})}function b(e){let{lazy:n,children:s,selectedValue:r}=e;const o=(Array.isArray(s)?s:[s]).filter(Boolean);if(n){const e=o.find((e=>e.props.value===r));return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,y.jsx)("div",{className:"margin-top--md",children:o.map(((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==r})))})}function w(e){const n=x(e);return(0,y.jsxs)("div",{className:(0,r.A)("tabs-container",j.tabList),children:[(0,y.jsx)(f,{...e,...n}),(0,y.jsx)(b,{...e,...n})]})}function v(e){const n=(0,g.A)();return(0,y.jsx)(w,{...e,children:u(e.children)},String(n))}},16316:(e,n,s)=>{s.d(n,{A:()=>t});const t=s.p+"assets/images/auth-architecture-b33065fc731227be200c1fb1a412bf37.png"},28453:(e,n,s)=>{s.d(n,{R:()=>i,x:()=>a});var t=s(96540);const r={},o=t.createContext(r);function i(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e857607a.a6e0d815.js b/assets/js/e857607a.a6e0d815.js deleted file mode 100644 index 26d45431fc..0000000000 --- a/assets/js/e857607a.a6e0d815.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[965],{21820:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>a,contentTitle:()=>r,default:()=>p,frontMatter:()=>c,metadata:()=>o,toc:()=>l});var t=n(74848),i=n(28453);const c={title:"Shell Scripts"},r=void 0,o={id:"cli/shell-scripts",title:"Shell Scripts",description:"Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play.",source:"@site/docs/cli/shell-scripts.md",sourceDirName:"cli",slug:"/cli/shell-scripts",permalink:"/docs/cli/shell-scripts",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/cli/shell-scripts.md",tags:[],version:"current",frontMatter:{title:"Shell Scripts"},sidebar:"someSidebar",previous:{title:"Commands",permalink:"/docs/cli/commands"},next:{title:"Code Generation",permalink:"/docs/cli/code-generation"}},a={},l=[{value:"Create Scripts",id:"create-scripts",level:2},{value:"Write Scripts",id:"write-scripts",level:2},{value:"Build and Run Scripts",id:"build-and-run-scripts",level:2}];function d(e){const s={blockquote:"blockquote",code:"code",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.p,{children:"Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play."}),"\n",(0,t.jsx)(s.h2,{id:"create-scripts",children:"Create Scripts"}),"\n",(0,t.jsxs)(s.p,{children:["A shell script is just a TypeScript file located in the ",(0,t.jsx)(s.code,{children:"src/scripts"}),". It must export a ",(0,t.jsx)(s.code,{children:"main"})," function that is then called when running the script."]}),"\n",(0,t.jsxs)(s.p,{children:["Let's create a new one with the command line: ",(0,t.jsx)(s.code,{children:"foal g script display-users"}),". A new file with a default template should appear in you ",(0,t.jsx)(s.code,{children:"src/scripts"})," directory."]}),"\n",(0,t.jsx)(s.h2,{id:"write-scripts",children:"Write Scripts"}),"\n",(0,t.jsxs)(s.p,{children:["Remove the content of ",(0,t.jsx)(s.code,{children:"src/scripts/display-users.ts"})," and replace it with the code below."]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { createService } from '@foal/core';\n\n// App\nimport { dataSource } from '../db';\nimport { User } from '../app/entities';\nimport { Logger } from '../app/services';\n\nexport async function main() {\n await dataSource.initialize();\n\n try {\n const users = await User.find();\n const logger = createService(Logger);\n logger.log(users);\n } finally {\n dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,t.jsxs)(s.p,{children:["As you can see, we can easily establish a connection to the database in the script as well as import some of the app components (the ",(0,t.jsx)(s.code,{children:"User"})," in this case)."]}),"\n",(0,t.jsxs)(s.p,{children:["Encapsulating your code in a ",(0,t.jsx)(s.code,{children:"main"})," function without calling it directly in the file has several benefits:"]}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["You can import and test your ",(0,t.jsx)(s.code,{children:"main"})," function in a separate file."]}),"\n",(0,t.jsx)(s.li,{children:"Using a function lets you easily use async/await keywords when dealing with asynchronous code."}),"\n"]}),"\n",(0,t.jsx)(s.h2,{id:"build-and-run-scripts",children:"Build and Run Scripts"}),"\n",(0,t.jsx)(s.p,{children:"To run a script you first need to build it."}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-sh",children:"npm run build\n"})}),"\n",(0,t.jsx)(s.p,{children:"Then you can execute it with this command:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-shell",children:"foal run my-script # or foal run-script my-script\n"})}),"\n",(0,t.jsxs)(s.blockquote,{children:["\n",(0,t.jsxs)(s.p,{children:["You can also provide additionnal arguments to your script (for example: ",(0,t.jsx)(s.code,{children:"foal run my-script foo=1 bar='[ 3, 4 ]'"}),"). The default template in the generated scripts shows you how to handle such behavior."]}),"\n"]}),"\n",(0,t.jsxs)(s.blockquote,{children:["\n",(0,t.jsxs)(s.p,{children:["If you want your script to recompile each time you save the file, you can run ",(0,t.jsx)(s.code,{children:"npm run dev"})," in a separate terminal."]}),"\n"]})]})}function p(e={}){const{wrapper:s}={...(0,i.R)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},28453:(e,s,n)=>{n.d(s,{R:()=>r,x:()=>o});var t=n(96540);const i={},c=t.createContext(i);function r(e){const s=t.useContext(c);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function o(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),t.createElement(c.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/e857607a.d83bf095.js b/assets/js/e857607a.d83bf095.js new file mode 100644 index 0000000000..1982abed47 --- /dev/null +++ b/assets/js/e857607a.d83bf095.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[965],{21820:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>a,contentTitle:()=>r,default:()=>p,frontMatter:()=>c,metadata:()=>o,toc:()=>l});var t=n(74848),i=n(28453);const c={title:"Shell Scripts"},r=void 0,o={id:"cli/shell-scripts",title:"Shell Scripts",description:"Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play.",source:"@site/docs/cli/shell-scripts.md",sourceDirName:"cli",slug:"/cli/shell-scripts",permalink:"/docs/cli/shell-scripts",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/cli/shell-scripts.md",tags:[],version:"current",frontMatter:{title:"Shell Scripts"},sidebar:"someSidebar",previous:{title:"Commands",permalink:"/docs/cli/commands"},next:{title:"Code Generation",permalink:"/docs/cli/code-generation"}},a={},l=[{value:"Create Scripts",id:"create-scripts",level:2},{value:"Write Scripts",id:"write-scripts",level:2},{value:"Build and Run Scripts",id:"build-and-run-scripts",level:2}];function d(e){const s={blockquote:"blockquote",code:"code",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.p,{children:"Sometimes we have to execute some tasks from the command line. These tasks can serve different purposes such as populating a database (user creation, etc) for instance. They often need to access some of the app classes and functions. This is when shell scripts come into play."}),"\n",(0,t.jsx)(s.h2,{id:"create-scripts",children:"Create Scripts"}),"\n",(0,t.jsxs)(s.p,{children:["A shell script is just a TypeScript file located in the ",(0,t.jsx)(s.code,{children:"src/scripts"}),". It must export a ",(0,t.jsx)(s.code,{children:"main"})," function that is then called when running the script."]}),"\n",(0,t.jsxs)(s.p,{children:["Let's create a new one with the command line: ",(0,t.jsx)(s.code,{children:"npx foal g script display-users"}),". A new file with a default template should appear in you ",(0,t.jsx)(s.code,{children:"src/scripts"})," directory."]}),"\n",(0,t.jsx)(s.h2,{id:"write-scripts",children:"Write Scripts"}),"\n",(0,t.jsxs)(s.p,{children:["Remove the content of ",(0,t.jsx)(s.code,{children:"src/scripts/display-users.ts"})," and replace it with the code below."]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-typescript",children:"// 3p\nimport { createService } from '@foal/core';\n\n// App\nimport { dataSource } from '../db';\nimport { User } from '../app/entities';\nimport { Logger } from '../app/services';\n\nexport async function main() {\n await dataSource.initialize();\n\n try {\n const users = await User.find();\n const logger = createService(Logger);\n logger.log(users);\n } finally {\n dataSource.destroy();\n }\n}\n\n"})}),"\n",(0,t.jsxs)(s.p,{children:["As you can see, we can easily establish a connection to the database in the script as well as import some of the app components (the ",(0,t.jsx)(s.code,{children:"User"})," in this case)."]}),"\n",(0,t.jsxs)(s.p,{children:["Encapsulating your code in a ",(0,t.jsx)(s.code,{children:"main"})," function without calling it directly in the file has several benefits:"]}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["You can import and test your ",(0,t.jsx)(s.code,{children:"main"})," function in a separate file."]}),"\n",(0,t.jsx)(s.li,{children:"Using a function lets you easily use async/await keywords when dealing with asynchronous code."}),"\n"]}),"\n",(0,t.jsx)(s.h2,{id:"build-and-run-scripts",children:"Build and Run Scripts"}),"\n",(0,t.jsx)(s.p,{children:"To run a script you first need to build it."}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-sh",children:"npm run build\n"})}),"\n",(0,t.jsx)(s.p,{children:"Then you can execute it with this command:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-shell",children:"npx foal run my-script # or npx foal run-script my-script\n"})}),"\n",(0,t.jsxs)(s.blockquote,{children:["\n",(0,t.jsxs)(s.p,{children:["You can also provide additionnal arguments to your script (for example: ",(0,t.jsx)(s.code,{children:"npx foal run my-script foo=1 bar='[ 3, 4 ]'"}),"). The default template in the generated scripts shows you how to handle such behavior."]}),"\n"]}),"\n",(0,t.jsxs)(s.blockquote,{children:["\n",(0,t.jsxs)(s.p,{children:["If you want your script to recompile each time you save the file, you can run ",(0,t.jsx)(s.code,{children:"npm run dev"})," in a separate terminal."]}),"\n"]})]})}function p(e={}){const{wrapper:s}={...(0,i.R)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},28453:(e,s,n)=>{n.d(s,{R:()=>r,x:()=>o});var t=n(96540);const i={},c=t.createContext(i);function r(e){const s=t.useContext(c);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function o(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),t.createElement(c.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/eb299cb3.ba8ab411.js b/assets/js/eb299cb3.e7bc3fd5.js similarity index 80% rename from assets/js/eb299cb3.ba8ab411.js rename to assets/js/eb299cb3.e7bc3fd5.js index 4bb801f163..93bb96c81a 100644 --- a/assets/js/eb299cb3.ba8ab411.js +++ b/assets/js/eb299cb3.e7bc3fd5.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3280],{23422:e=>{e.exports=JSON.parse('{"permalink":"/blog/tags/release/page/2","page":2,"postsPerPage":10,"totalPages":3,"totalCount":24,"previousPage":"/blog/tags/release","nextPage":"/blog/tags/release/page/3","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[3280],{23422:e=>{e.exports=JSON.parse('{"permalink":"/blog/tags/release/page/2","page":2,"postsPerPage":10,"totalPages":3,"totalCount":25,"previousPage":"/blog/tags/release","nextPage":"/blog/tags/release/page/3","blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/assets/js/f819756d.a51687a2.js b/assets/js/f819756d.1b3293d5.js similarity index 53% rename from assets/js/f819756d.a51687a2.js rename to assets/js/f819756d.1b3293d5.js index ff1f5cb4b6..0cb1122327 100644 --- a/assets/js/f819756d.a51687a2.js +++ b/assets/js/f819756d.1b3293d5.js @@ -1 +1 @@ -"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2521],{66135:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var o=t(74848),r=t(28453);const s={title:"Social Auth with Google",id:"tuto-15-social-auth",slug:"15-social-auth"},l=void 0,i={id:"tutorials/real-world-example-with-react/tuto-15-social-auth",title:"Social Auth with Google",description:"In this last part of the tutorial, we will give users the ability to log in with Google. Currently, they can only log in with an email and a password.",source:"@site/docs/tutorials/real-world-example-with-react/15-social-auth.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/15-social-auth",permalink:"/docs/tutorials/real-world-example-with-react/15-social-auth",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md",tags:[],version:"current",sidebarPosition:15,frontMatter:{title:"Social Auth with Google",id:"tuto-15-social-auth",slug:"15-social-auth"},sidebar:"someSidebar",previous:{title:"Production Build",permalink:"/docs/tutorials/real-world-example-with-react/14-production-build"},next:{title:"Architecture Overview",permalink:"/docs/architecture/architecture-overview"}},a={},c=[{value:"Nullable Passwords",id:"nullable-passwords",level:2},{value:"Configuration",id:"configuration",level:2},{value:"The Social Controller",id:"the-social-controller",level:2}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.p,{children:"In this last part of the tutorial, we will give users the ability to log in with Google. Currently, they can only log in with an email and a password."}),"\n",(0,o.jsx)(n.p,{children:"To do this, you will use Foal's social authentication system."}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsx)(n.p,{children:(0,o.jsxs)(n.em,{children:["This section assumes that you have already set up a Google application and have retrieved your client ID and secret. If you have not, you might want to check this ",(0,o.jsx)(n.a,{href:"/docs/authentication/social-auth",children:"page"})," first. The redirection URIs allowed in your Google application must include ",(0,o.jsx)(n.code,{children:"http://localhost:3001/api/auth/google/callback"}),"."]})}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"nullable-passwords",children:"Nullable Passwords"}),"\n",(0,o.jsxs)(n.p,{children:["The first step is to update the ",(0,o.jsx)(n.code,{children:"User"})," model. Some users may only use the social login and therefore not have a password. To take this into account, we will make the ",(0,o.jsx)(n.code,{children:"password"})," column accept null values."]}),"\n",(0,o.jsxs)(n.p,{children:["Open ",(0,o.jsx)(n.code,{children:"user.entity.ts"})," and update its contents."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column({ nullable: true })\n password: string;\n\n @Column()\n name: string;\n\n @Column()\n avatar: string;\n\n}\n\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,o.jsx)(n.p,{children:"Make and run the migrations."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Then open ",(0,o.jsx)(n.code,{children:"auth.controller.ts"})," and add a condition to check whether the password value is ",(0,o.jsx)(n.code,{children:"null"})," in the database."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"if (!user.password) {\n return new HttpResponseUnauthorized();\n}\n\nif (!(await verifyPassword(password, user.password))) {\n return new HttpResponseUnauthorized();\n}\n"})}),"\n",(0,o.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsx)(n.p,{children:"Now that the password problem is solved, you can install the packages and provide your social credentials in the configuration."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install @foal/social node-fetch@2\n"})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.json"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "port": "env(PORT)",\n "settings": {\n ...\n "social": {\n "google": {\n "clientId": "env(GOOGLE_CLIENT_ID)",\n "clientSecret": "env(GOOGLE_CLIENT_SECRET)",\n "redirectUri": "http://localhost:3001/api/auth/google/callback"\n }\n },\n },\n ...\n}\n'})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:".env"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'# ...\n\nGOOGLE_CLIENT_ID="your Google client ID"\nGOOGLE_CLIENT_SECRET="your Google client secret"\n'})}),"\n",(0,o.jsx)(n.h2,{id:"the-social-controller",children:"The Social Controller"}),"\n",(0,o.jsx)(n.p,{children:"Create the controller."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"foal generate controller api/social-auth --register\n"})}),"\n",(0,o.jsx)(n.p,{children:"Open the file and add two new routes."}),"\n",(0,o.jsxs)(n.table,{children:[(0,o.jsx)(n.thead,{children:(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.th,{children:"API endpoint"}),(0,o.jsx)(n.th,{children:"Method"}),(0,o.jsx)(n.th,{children:"Description"})]})}),(0,o.jsxs)(n.tbody,{children:[(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"/api/auth/google"})}),(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"POST"})}),(0,o.jsx)(n.td,{children:"Redirects the user to Google login page."})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"/api/auth/google/callback"})}),(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"GET"})}),(0,o.jsx)(n.td,{children:"Handles redirection from Google once the user has approved the connection."})]})]})]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Context, dependency, Get, HttpResponseRedirect } from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\nimport { User } from '../../entities';\nimport * as fetch from 'node-fetch';\nimport { Disk } from '@foal/storage';\n\ninterface GoogleUserInfo {\n email: string;\n name?: string;\n picture?: string;\n}\n\nexport class SocialAuthController {\n @dependency\n google: GoogleProvider;\n\n @dependency\n disk: Disk;\n\n @Get('/google')\n redirectToGoogle() {\n return this.google.redirect();\n }\n\n @Get('/google/callback')\n async handleGoogleRedirection(ctx: Context) {\n const { userInfo } = await this.google.getUserInfo(ctx);\n\n if (!userInfo.email) {\n throw new Error('Google should have returned an email address.');\n }\n\n let user = await User.findOneBy({ email: userInfo.email });\n\n if (!user) {\n user = new User();\n user.email = userInfo.email;\n user.avatar = '';\n user.name = userInfo.name ?? 'Unknown';\n\n if (userInfo.picture) {\n const response = await fetch(userInfo.picture);\n const { path } = await this.disk.write('images/profiles/uploaded', response.body)\n user.avatar = path;\n }\n\n await user.save();\n }\n\n ctx.session!.setUser(user);\n ctx.user = user;\n\n return new HttpResponseRedirect('/');\n }\n\n}\n\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Open ",(0,o.jsx)(n.code,{children:"api.controller.ts"})," and replace the path prefix of the ",(0,o.jsx)(n.code,{children:"SocialAuthController"})," with ",(0,o.jsx)(n.code,{children:"/auth"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"subControllers = [\n controller('/stories', StoriesController),\n controller('/auth', AuthController),\n controller('/profile', ProfileController),\n controller('/auth', SocialAuthController)\n];\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Go to ",(0,o.jsx)(n.a,{href:"http://localhost:3001/login",children:"http://localhost:3001/login"})," and click on the ",(0,o.jsx)(n.em,{children:"Connect with Google"})," button. You are redirected to the Google login page. Once you have validated the connection, you will be redirected to the home page. If you have a Google profile picture, you will see it on your profile page."]}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsxs)(n.p,{children:["For this to work, you need to make sure you are using port ",(0,o.jsx)(n.code,{children:"3001"})," to test the social login. This assumes that you created the production build in the previous step of this tutorial. You can't use the React development server here because the redirects won't work with both ports ",(0,o.jsx)(n.code,{children:"3000"})," and ",(0,o.jsx)(n.code,{children:"3001"}),"."]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["Congratulations! You have reached the end of this tutorial. You can find the complete source code ",(0,o.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:t(44005).A+"",children:"here"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},44005:(e,n,t)=>{t.d(n,{A:()=>o});const o=t.p+"assets/files/tutorial-foal-react-38621ac35b14eaf0d07f613929e7e98b.zip"},28453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>i});var o=t(96540);const r={},s=o.createContext(r);function l(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:l(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file +"use strict";(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[2521],{66135:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>c});var o=t(74848),r=t(28453);const s={title:"Social Auth with Google",id:"tuto-15-social-auth",slug:"15-social-auth"},l=void 0,i={id:"tutorials/real-world-example-with-react/tuto-15-social-auth",title:"Social Auth with Google",description:"In this last part of the tutorial, we will give users the ability to log in with Google. Currently, they can only log in with an email and a password.",source:"@site/docs/tutorials/real-world-example-with-react/15-social-auth.md",sourceDirName:"tutorials/real-world-example-with-react",slug:"/tutorials/real-world-example-with-react/15-social-auth",permalink:"/docs/tutorials/real-world-example-with-react/15-social-auth",draft:!1,unlisted:!1,editUrl:"https://github.com/FoalTS/foal/edit/master/docs/docs/tutorials/real-world-example-with-react/15-social-auth.md",tags:[],version:"current",sidebarPosition:15,frontMatter:{title:"Social Auth with Google",id:"tuto-15-social-auth",slug:"15-social-auth"},sidebar:"someSidebar",previous:{title:"Production Build",permalink:"/docs/tutorials/real-world-example-with-react/14-production-build"},next:{title:"Architecture Overview",permalink:"/docs/architecture/architecture-overview"}},a={},c=[{value:"Nullable Passwords",id:"nullable-passwords",level:2},{value:"Configuration",id:"configuration",level:2},{value:"The Social Controller",id:"the-social-controller",level:2}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.p,{children:"In this last part of the tutorial, we will give users the ability to log in with Google. Currently, they can only log in with an email and a password."}),"\n",(0,o.jsx)(n.p,{children:"To do this, you will use Foal's social authentication system."}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsx)(n.p,{children:(0,o.jsxs)(n.em,{children:["This section assumes that you have already set up a Google application and have retrieved your client ID and secret. If you have not, you might want to check this ",(0,o.jsx)(n.a,{href:"/docs/authentication/social-auth",children:"page"})," first. The redirection URIs allowed in your Google application must include ",(0,o.jsx)(n.code,{children:"http://localhost:3001/api/auth/google/callback"}),"."]})}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"nullable-passwords",children:"Nullable Passwords"}),"\n",(0,o.jsxs)(n.p,{children:["The first step is to update the ",(0,o.jsx)(n.code,{children:"User"})," model. Some users may only use the social login and therefore not have a password. To take this into account, we will make the ",(0,o.jsx)(n.code,{children:"password"})," column accept null values."]}),"\n",(0,o.jsxs)(n.p,{children:["Open ",(0,o.jsx)(n.code,{children:"user.entity.ts"})," and update its contents."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';\n\n@Entity()\nexport class User extends BaseEntity {\n\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column({ unique: true })\n email: string;\n\n @Column({ nullable: true })\n password: string;\n\n @Column()\n name: string;\n\n @Column()\n avatar: string;\n\n}\n\nexport { DatabaseSession } from '@foal/typeorm';\n"})}),"\n",(0,o.jsx)(n.p,{children:"Make and run the migrations."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm run makemigrations\nnpm run migrations\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Then open ",(0,o.jsx)(n.code,{children:"auth.controller.ts"})," and add a condition to check whether the password value is ",(0,o.jsx)(n.code,{children:"null"})," in the database."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"if (!user.password) {\n return new HttpResponseUnauthorized();\n}\n\nif (!(await verifyPassword(password, user.password))) {\n return new HttpResponseUnauthorized();\n}\n"})}),"\n",(0,o.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsx)(n.p,{children:"Now that the password problem is solved, you can install the packages and provide your social credentials in the configuration."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install @foal/social node-fetch@2\n"})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:"config/default.json"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "port": "env(PORT)",\n "settings": {\n ...\n "social": {\n "google": {\n "clientId": "env(GOOGLE_CLIENT_ID)",\n "clientSecret": "env(GOOGLE_CLIENT_SECRET)",\n "redirectUri": "http://localhost:3001/api/auth/google/callback"\n }\n },\n },\n ...\n}\n'})}),"\n",(0,o.jsx)(n.p,{children:(0,o.jsx)(n.em,{children:".env"})}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'# ...\n\nGOOGLE_CLIENT_ID="your Google client ID"\nGOOGLE_CLIENT_SECRET="your Google client secret"\n'})}),"\n",(0,o.jsx)(n.h2,{id:"the-social-controller",children:"The Social Controller"}),"\n",(0,o.jsx)(n.p,{children:"Create the controller."}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npx foal generate controller api/social-auth --register\n"})}),"\n",(0,o.jsx)(n.p,{children:"Open the file and add two new routes."}),"\n",(0,o.jsxs)(n.table,{children:[(0,o.jsx)(n.thead,{children:(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.th,{children:"API endpoint"}),(0,o.jsx)(n.th,{children:"Method"}),(0,o.jsx)(n.th,{children:"Description"})]})}),(0,o.jsxs)(n.tbody,{children:[(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"/api/auth/google"})}),(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"POST"})}),(0,o.jsx)(n.td,{children:"Redirects the user to Google login page."})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"/api/auth/google/callback"})}),(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"GET"})}),(0,o.jsx)(n.td,{children:"Handles redirection from Google once the user has approved the connection."})]})]})]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Context, dependency, Get, HttpResponseRedirect } from '@foal/core';\nimport { GoogleProvider } from '@foal/social';\nimport { User } from '../../entities';\nimport * as fetch from 'node-fetch';\nimport { Disk } from '@foal/storage';\n\ninterface GoogleUserInfo {\n email: string;\n name?: string;\n picture?: string;\n}\n\nexport class SocialAuthController {\n @dependency\n google: GoogleProvider;\n\n @dependency\n disk: Disk;\n\n @Get('/google')\n redirectToGoogle() {\n return this.google.createHttpResponseWithConsentPageUrl({ isRedirection: true });\n }\n\n @Get('/google/callback')\n async handleGoogleRedirection(ctx: Context) {\n const { userInfo } = await this.google.getUserInfo(ctx);\n\n if (!userInfo.email) {\n throw new Error('Google should have returned an email address.');\n }\n\n let user = await User.findOneBy({ email: userInfo.email });\n\n if (!user) {\n user = new User();\n user.email = userInfo.email;\n user.avatar = '';\n user.name = userInfo.name ?? 'Unknown';\n\n if (userInfo.picture) {\n const response = await fetch(userInfo.picture);\n const { path } = await this.disk.write('images/profiles/uploaded', response.body)\n user.avatar = path;\n }\n\n await user.save();\n }\n\n ctx.session!.setUser(user);\n ctx.user = user;\n\n return new HttpResponseRedirect('/');\n }\n\n}\n\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Open ",(0,o.jsx)(n.code,{children:"api.controller.ts"})," and replace the path prefix of the ",(0,o.jsx)(n.code,{children:"SocialAuthController"})," with ",(0,o.jsx)(n.code,{children:"/auth"}),"."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"subControllers = [\n controller('/stories', StoriesController),\n controller('/auth', AuthController),\n controller('/profile', ProfileController),\n controller('/auth', SocialAuthController)\n];\n"})}),"\n",(0,o.jsxs)(n.p,{children:["Go to ",(0,o.jsx)(n.a,{href:"http://localhost:3001/login",children:"http://localhost:3001/login"})," and click on the ",(0,o.jsx)(n.em,{children:"Connect with Google"})," button. You are redirected to the Google login page. Once you have validated the connection, you will be redirected to the home page. If you have a Google profile picture, you will see it on your profile page."]}),"\n",(0,o.jsxs)(n.blockquote,{children:["\n",(0,o.jsxs)(n.p,{children:["For this to work, you need to make sure you are using port ",(0,o.jsx)(n.code,{children:"3001"})," to test the social login. This assumes that you created the production build in the previous step of this tutorial. You can't use the React development server here because the redirects won't work with both ports ",(0,o.jsx)(n.code,{children:"3000"})," and ",(0,o.jsx)(n.code,{children:"3001"}),"."]}),"\n"]}),"\n",(0,o.jsxs)(n.p,{children:["Congratulations! You have reached the end of this tutorial. You can find the complete source code ",(0,o.jsx)(n.a,{target:"_blank","data-noBrokenLinkCheck":!0,href:t(44005).A+"",children:"here"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},44005:(e,n,t)=>{t.d(n,{A:()=>o});const o=t.p+"assets/files/tutorial-foal-react-38621ac35b14eaf0d07f613929e7e98b.zip"},28453:(e,n,t)=>{t.d(n,{R:()=>l,x:()=>i});var o=t(96540);const r={},s=o.createContext(r);function l(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:l(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/main.13d0dec4.js b/assets/js/main.13d0dec4.js new file mode 100644 index 0000000000..28b9f82f80 --- /dev/null +++ b/assets/js/main.13d0dec4.js @@ -0,0 +1,2 @@ +/*! For license information please see main.13d0dec4.js.LICENSE.txt */ +(self.webpackChunkdocs=self.webpackChunkdocs||[]).push([[8792],{89188:(e,t,n)=>{"use strict";n.d(t,{W:()=>r});var o=n(96540);function r(){return o.createElement("svg",{width:"20",height:"20",className:"DocSearch-Search-Icon",viewBox:"0 0 20 20","aria-hidden":"true"},o.createElement("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}},35947:(e,t,n)=>{"use strict";n.d(t,{A:()=>p});n(96540);var o=n(53259),r=n.n(o),a=n(84054);const i={"00457910":[()=>n.e(3129).then(n.bind(n,63636)),"@site/versioned_docs/version-2.x/cookbook/not-found-page.md",63636],"00f43032":[()=>Promise.all([n.e(1869),n.e(7383)]).then(n.bind(n,61830)),"@site/versioned_docs/version-3.x/security/body-size-limiting.md",61830],"01a85c17":[()=>Promise.all([n.e(1869),n.e(8209)]).then(n.bind(n,69158)),"@theme/BlogTagsListPage",69158],"01c19473":[()=>Promise.all([n.e(1869),n.e(7639)]).then(n.bind(n,98529)),"@site/versioned_docs/version-2.x/comparison-with-other-frameworks/express-fastify.md",98529],"02047eff":[()=>n.e(6985).then(n.bind(n,63848)),"@site/blog/2022-06-13-FoalTS-2022-survey-is-open.md",63848],"02715c9e":[()=>n.e(8083).then(n.t.bind(n,18890,19)),"~docs/default/version-1-x-metadata-prop-27c.json",18890],"03563ade":[()=>n.e(8708).then(n.bind(n,17957)),"@site/versioned_docs/version-1.x/testing/unit-testing.md",17957],"03bb6fd4":[()=>n.e(7964).then(n.bind(n,92522)),"@site/versioned_docs/version-1.x/security/xss-protection.md",92522],"0451242e":[()=>n.e(6572).then(n.bind(n,15739)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/12-file-upload.md",15739],"048ae7b9":[()=>Promise.all([n.e(1869),n.e(4321)]).then(n.bind(n,18698)),"@site/versioned_docs/version-3.x/frontend/server-side-rendering.md",18698],"05774ae8":[()=>n.e(1258).then(n.bind(n,75782)),"@site/versioned_docs/version-1.x/databases/create-models-and-queries.md",75782],"05f6ad57":[()=>n.e(8161).then(n.bind(n,20790)),"@site/docs/tutorials/simple-todo-list/2-introduction.md",20790],"06464094":[()=>n.e(1336).then(n.bind(n,51275)),"@site/versioned_docs/version-3.x/common/serialization.md",51275],"07b74290":[()=>Promise.all([n.e(1869),n.e(4620)]).then(n.bind(n,58496)),"@site/versioned_docs/version-2.x/cookbook/request-body-size.md",58496],"0829693d":[()=>n.e(6382).then(n.bind(n,20813)),"@site/versioned_docs/version-1.x/README.md",20813],"083c259c":[()=>Promise.all([n.e(1869),n.e(2546)]).then(n.bind(n,17174)),"@site/versioned_docs/version-2.x/architecture/services-and-dependency-injection.md",17174],"08a99fec":[()=>n.e(1080).then(n.bind(n,51078)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-7-unit-testing.md",51078],"09444585":[()=>Promise.all([n.e(1869),n.e(2658)]).then(n.bind(n,43966)),"@site/docs/common/openapi-and-swagger-ui.md",43966],"09e23a09":[()=>n.e(9314).then(n.t.bind(n,52582,19)),"~blog/default/blog-tags-release-page-3-4c9.json",52582],"0a01f85d":[()=>n.e(62).then(n.bind(n,29620)),"@site/blog/2022-06-13-FoalTS-2022-survey-is-open.md?truncated=true",29620],"0b25a698":[()=>n.e(8672).then(n.bind(n,70559)),"@site/versioned_docs/version-2.x/api-section/gRPC.md",70559],"0bab0724":[()=>n.e(6649).then(n.bind(n,26862)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-5-the-rest-api.md",26862],"0bd56803":[()=>n.e(7189).then(n.bind(n,36180)),"@site/versioned_docs/version-3.x/databases/typeorm/generate-and-run-migrations.md",36180],"0ce2a69a":[()=>n.e(8986).then(n.bind(n,60499)),"@site/versioned_docs/version-2.x/common/conversions.md",60499],"0dbb9cb3":[()=>n.e(7194).then(n.bind(n,60073)),"@site/versioned_docs/version-2.x/frontend-integration/angular-react-vue.md",60073],"0e1f8ec8":[()=>n.e(939).then(n.bind(n,64704)),"@site/versioned_docs/version-3.x/testing/introduction.md",64704],"0ee830f3":[()=>n.e(3633).then(n.bind(n,438)),"@site/docs/tutorials/real-world-example-with-react/1-introduction.md",438],"0f694e49":[()=>n.e(4946).then(n.bind(n,88358)),"@site/blog/2021-06-11-version-2.5-release-notes.md",88358],"0f93558a":[()=>n.e(3386).then(n.bind(n,41586)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/5-our-first-route.md",41586],"10b24d0f":[()=>n.e(2718).then(n.bind(n,65675)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/1-introduction.md",65675],"10eec10b":[()=>n.e(1680).then(n.bind(n,47089)),"@site/versioned_docs/version-2.x/architecture/controllers.md",47089],"114be409":[()=>n.e(7417).then(n.bind(n,54011)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-1-installation.md",54011],"11b798b2":[()=>Promise.all([n.e(1869),n.e(4307)]).then(n.bind(n,64953)),"@site/blog/2021-03-11-whats-new-in-version-2-part-3.md",64953],"11edeb5f":[()=>n.e(5354).then(n.bind(n,57314)),"@site/blog/2023-09-11-version-4.0-release-notes.md",57314],"1271e772":[()=>n.e(2377).then(n.bind(n,34305)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-8-e2e-testing-and-authentication.md",34305],"12b8e610":[()=>n.e(9662).then(n.bind(n,34373)),"@site/docs/testing/introduction.md",34373],"1301d509":[()=>n.e(8422).then(n.bind(n,34186)),"@site/docs/databases/typeorm/introduction.md",34186],"13d48814":[()=>n.e(9384).then(n.bind(n,8090)),"@site/versioned_docs/version-2.x/testing/unit-testing.md",8090],"141d3f50":[()=>n.e(1531).then(n.bind(n,67796)),"@site/versioned_docs/version-2.x/databases/using-another-orm.md",67796],"14bd523d":[()=>n.e(5146).then(n.bind(n,53583)),"@site/versioned_docs/version-3.x/README.md",53583],"15060a2e":[()=>n.e(9413).then(n.bind(n,90687)),"@site/versioned_docs/version-2.x/development-environment/create-and-run-scripts.md",90687],"15516f50":[()=>n.e(1956).then(n.bind(n,25142)),"@site/docs/common/utilities.md",25142],"155c242a":[()=>n.e(9660).then(n.bind(n,74150)),"@site/versioned_docs/version-1.x/cookbook/root-imports.md",74150],"15ea3f76":[()=>Promise.all([n.e(1869),n.e(1179)]).then(n.bind(n,67314)),"@site/blog/2021-03-02-whats-new-in-version-2-part-2.md?truncated=true",67314],"163c81f1":[()=>n.e(4994).then(n.bind(n,96368)),"@site/docs/architecture/hooks.md",96368],"177cb55b":[()=>Promise.all([n.e(1869),n.e(9311)]).then(n.bind(n,20859)),"@site/versioned_docs/version-2.x/databases/typeorm.md",20859],17896441:[()=>Promise.all([n.e(1869),n.e(8222),n.e(8401)]).then(n.bind(n,25022)),"@theme/DocItem",25022],"17de4ea4":[()=>n.e(3428).then(n.bind(n,48178)),"@site/docs/tutorials/real-world-example-with-react/7-add-frontend.md",48178],"1812b504":[()=>Promise.all([n.e(1869),n.e(9990)]).then(n.bind(n,87101)),"@site/versioned_docs/version-3.x/databases/typeorm/mongodb.md",87101],"1877539e":[()=>n.e(1172).then(n.bind(n,86127)),"@site/docs/common/graphql.md",86127],"18a9acb6":[()=>n.e(2314).then(n.bind(n,26103)),"@site/versioned_docs/version-1.x/api-section/public-api-and-cors-requests.md",26103],"19cf03af":[()=>n.e(8048).then(n.t.bind(n,61966,19)),"/home/runner/work/foal/foal/docs/.docusaurus/docusaurus-plugin-content-docs/default/plugin-route-context-module-100.json",61966],"1a4e3797":[()=>Promise.all([n.e(1869),n.e(2138)]).then(n.bind(n,74604)),"@theme/SearchPage",74604],"1a6a4e35":[()=>n.e(6931).then(n.bind(n,91508)),"@site/blog/2021-09-19-version-2.6-release-notes.md?truncated=true",91508],"1adfb366":[()=>n.e(9586).then(n.bind(n,18063)),"@site/versioned_docs/version-2.x/security/xss-protection.md",18063],"1ae9e0b4":[()=>n.e(9354).then(n.bind(n,72242)),"@site/versioned_docs/version-1.x/testing/introduction.md",72242],"1b19a422":[()=>n.e(9719).then(n.bind(n,55594)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-5-the-rest-api.md",55594],"1b37eeaa":[()=>n.e(7464).then(n.bind(n,48935)),"@site/versioned_docs/version-3.x/common/gRPC.md",48935],"1b53d3a5":[()=>n.e(2275).then(n.bind(n,72590)),"@site/docs/testing/e2e-testing.md",72590],"1bb97f20":[()=>n.e(548).then(n.bind(n,72652)),"@site/blog/2022-11-28-version-3.1-release-notes.md?truncated=true",72652],"1bc14fa0":[()=>n.e(3875).then(n.bind(n,9747)),"@site/blog/2022-05-29-version-2.9-release-notes.md",9747],"1c19e1e5":[()=>n.e(2909).then(n.bind(n,79701)),"@site/blog/2023-08-13-version-3.3-release-notes.md",79701],"1cd10a72":[()=>n.e(3616).then(n.bind(n,85234)),"@site/versioned_docs/version-1.x/file-system/upload-and-download-files.md",85234],"1cf91b15":[()=>n.e(6115).then(n.bind(n,62463)),"@site/src/pages/newsletter.jsx",62463],"1e48c1bc":[()=>n.e(3508).then(n.bind(n,20924)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/13-csrf.md",20924],20141232:[()=>n.e(3340).then(n.bind(n,34112)),"@site/blog/2022-11-28-version-3.1-release-notes.md",34112],"202275df":[()=>n.e(2183).then(n.bind(n,96198)),"@site/versioned_docs/version-1.x/deployment-and-environments/ship-to-production.md",96198],"20548c92":[()=>n.e(6316).then(n.bind(n,11397)),"@site/blog/2022-05-29-version-2.9-release-notes.md?truncated=true",11397],"208181e4":[()=>Promise.all([n.e(1869),n.e(6178)]).then(n.bind(n,86970)),"@site/versioned_docs/version-3.x/authentication/social-auth.md",86970],"20919c84":[()=>n.e(8671).then(n.bind(n,47156)),"@site/versioned_docs/version-2.x/upgrade-to-v2/validation-hooks.md",47156],"20a7d101":[()=>n.e(9057).then(n.bind(n,30983)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/13-csrf.md",30983],"215c9b58":[()=>n.e(9466).then(n.bind(n,77063)),"@site/versioned_docs/version-2.x/cookbook/limit-repeated-requests.md",77063],"2188c923":[()=>n.e(81).then(n.bind(n,96386)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-7-unit-testing.md",96386],"21ef02f2":[()=>n.e(6755).then(n.bind(n,77135)),"@site/versioned_docs/version-1.x/development-environment/code-generation.md",77135],"2228e83c":[()=>n.e(2040).then(n.bind(n,71197)),"@site/versioned_docs/version-2.x/development-environment/build-and-start-the-app.md",71197],"23374ca6":[()=>n.e(2278).then(n.bind(n,94842)),"@site/docs/README.md",94842],23716945:[()=>n.e(5404).then(n.bind(n,19404)),"@site/versioned_docs/version-1.x/validation-and-sanitization.md",19404],"237c2dd2":[()=>n.e(5264).then(n.bind(n,11466)),"@site/versioned_docs/version-2.x/upgrade-to-v2/custom-express-instance.md",11466],"23af3712":[()=>Promise.all([n.e(1869),n.e(9856)]).then(n.bind(n,29358)),"@site/blog/2022-02-13-version-2.8-release-notes.md?truncated=true",29358],"23afd9db":[()=>n.e(436).then(n.bind(n,96834)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/10-auth-with-react.md",96834],"23b1c6d9":[()=>n.e(9571).then(n.bind(n,96006)),"@site/versioned_docs/version-2.x/README.md",96006],"2566c0b5":[()=>n.e(1962).then(n.bind(n,86679)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-3-the-todo-model.md",86679],"25677f9e":[()=>n.e(8636).then(n.bind(n,45372)),"@site/versioned_docs/version-2.x/databases/generate-and-run-migrations.md",45372],"25a3b5d4":[()=>Promise.all([n.e(1869),n.e(5851)]).then(n.bind(n,26553)),"@site/versioned_docs/version-3.x/common/openapi-and-swagger-ui.md",26553],"2615f5bc":[()=>n.e(6971).then(n.bind(n,81440)),"@site/docs/cli/linting-and-code-style.md",81440],"265debd3":[()=>Promise.all([n.e(1869),n.e(5210)]).then(n.bind(n,39791)),"@site/docs/frontend/server-side-rendering.md",39791],"26bc6660":[()=>n.e(300).then(n.bind(n,16840)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/7-unit-testing.md",16840],"276b2eb5":[()=>n.e(3870).then(n.bind(n,91064)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/3-the-models.md",91064],"2788c7dd":[()=>n.e(1360).then(n.bind(n,94722)),"@site/versioned_docs/version-3.x/cli/code-generation.md",94722],"27cbb96a":[()=>n.e(6229).then(n.bind(n,7989)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/9-authenticated-api.md",7989],"28007a2a":[()=>n.e(2410).then(n.bind(n,88178)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/8-authentication.md",88178],"297e34ad":[()=>n.e(5186).then(n.bind(n,70675)),"@site/docs/authorization/groups-and-permissions.md",70675],"2a3fc7ce":[()=>n.e(9357).then(n.bind(n,75900)),"@site/versioned_docs/version-2.x/upgrade-to-v2/openapi.md",75900],"2aa816ee":[()=>n.e(3948).then(n.bind(n,73634)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/1-introduction.md",73634],"2b003a7b":[()=>Promise.all([n.e(1869),n.e(7872)]).then(n.bind(n,71829)),"@site/versioned_docs/version-2.x/databases/mongodb.md",71829],"2ba33725":[()=>n.e(6250).then(n.bind(n,43656)),"@site/versioned_docs/version-2.x/api-section/public-api-and-cors-requests.md",43656],"2bb31afe":[()=>n.e(4695).then(n.bind(n,38643)),"@site/docs/frontend/not-found-page.md",38643],"2e423443":[()=>n.e(3416).then(n.bind(n,7017)),"@site/blog/2024-04-25-version-4.4-release-notes.md",7017],"2e47d16a":[()=>n.e(2685).then(n.bind(n,49814)),"@site/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",49814],"2fef57eb":[()=>n.e(3671).then(n.bind(n,76778)),"@site/versioned_docs/version-1.x/frontend-integration/angular-react-vue.md",76778],"30471b97":[()=>n.e(6728).then(n.bind(n,38167)),"@site/blog/2023-10-29-version-4.2-release-notes.md?truncated=true",38167],"30fdc1af":[()=>Promise.all([n.e(1869),n.e(7169)]).then(n.bind(n,23548)),"@site/versioned_docs/version-3.x/common/file-storage/upload-and-download-files.md",23548],"312523e1":[()=>n.e(3155).then(n.bind(n,25588)),"@site/docs/tutorials/simple-todo-list/5-the-rest-api.md",25588],"31eb4fb4":[()=>n.e(4414).then(n.bind(n,23858)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/4-the-shell-scripts.md",23858],"32ad2676":[()=>n.e(2988).then(n.bind(n,61143)),"@site/docs/tutorials/real-world-example-with-react/2-database-set-up.md",61143],"338a5749":[()=>n.e(4005).then(n.bind(n,23242)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/9-authenticated-api.md",23242],"33f3ea23":[()=>n.e(4039).then(n.bind(n,12558)),"@site/blog/2022-11-01-version-3.0-release-notes.md",12558],"341027c1":[()=>n.e(7892).then(n.bind(n,84283)),"@site/docs/databases/typeorm/generate-and-run-migrations.md",84283],"347b2a96":[()=>Promise.all([n.e(1869),n.e(8551)]).then(n.bind(n,29391)),"@site/versioned_docs/version-2.x/security/csrf-protection.md",29391],"34b70249":[()=>n.e(2656).then(n.bind(n,63095)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/7-unit-testing.md",63095],"354711d0":[()=>n.e(3458).then(n.bind(n,93979)),"@site/versioned_docs/version-1.x/architecture/initialization.md",93979],"356b6cd7":[()=>n.e(3933).then(n.bind(n,52876)),"@site/docs/authentication/password-management.md",52876],"35f1d7a1":[()=>n.e(961).then(n.bind(n,24170)),"@site/versioned_docs/version-1.x/architecture/controllers.md",24170],"374ac0f8":[()=>n.e(6871).then(n.bind(n,68568)),"@site/versioned_docs/version-2.x/upgrade-to-v2/jwt-and-csrf.md",68568],"38e4d1eb":[()=>n.e(3813).then(n.bind(n,72325)),"@site/versioned_docs/version-1.x/databases/generate-and-run-migrations.md",72325],"39e0eb28":[()=>Promise.all([n.e(1869),n.e(2003)]).then(n.bind(n,68833)),"@site/versioned_docs/version-2.x/common/validation-and-sanitization.md",68833],"3bf2279e":[()=>n.e(1091).then(n.bind(n,51897)),"@site/versioned_docs/version-2.x/cookbook/expressjs.md",51897],"3c3b6fb9":[()=>n.e(3505).then(n.t.bind(n,17445,19)),"~blog/default/blog-tags-survey-f63-list.json",17445],"3fbe8240":[()=>Promise.all([n.e(1869),n.e(5953)]).then(n.bind(n,55874)),"@site/docs/databases/typeorm/mongodb.md",55874],"40d28471":[()=>n.e(2517).then(n.bind(n,18832)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-2-introduction.md",18832],"42632ea5":[()=>n.e(8593).then(n.bind(n,64967)),"@site/versioned_docs/version-3.x/security/cors.md",64967],"433c26e6":[()=>n.e(3207).then(n.bind(n,2630)),"@site/versioned_docs/version-1.x/deployment-and-environments/configuration.md",2630],"436a1b0b":[()=>n.e(8872).then(n.bind(n,22293)),"@site/blog/2023-10-29-version-4.2-release-notes.md",22293],"43a4db71":[()=>n.e(9717).then(n.bind(n,19805)),"@site/docs/security/rate-limiting.md",19805],"44b87ee7":[()=>n.e(5985).then(n.bind(n,84146)),"@site/versioned_docs/version-1.x/api-section/openapi-and-swagger-ui.md",84146],"45aab7e5":[()=>n.e(8839).then(n.t.bind(n,66346,19)),"~docs/default/version-2-x-metadata-prop-4fa.json",66346],"45dbd969":[()=>n.e(1682).then(n.bind(n,90509)),"@site/docs/architecture/controllers.md",90509],"465e23c3":[()=>n.e(8852).then(n.bind(n,56910)),"@site/versioned_docs/version-2.x/community/awesome-foal.md",56910],"4716f7ee":[()=>n.e(9282).then(n.bind(n,39942)),"@site/docs/tutorials/real-world-example-with-react/11-sign-up.md",39942],"472b3722":[()=>n.e(6162).then(n.bind(n,81039)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/installation-troubleshooting.md",81039],"4748bbf4":[()=>n.e(7178).then(n.bind(n,20818)),"@site/versioned_docs/version-2.x/testing/e2e-testing.md",20818],"47e68ea2":[()=>n.e(6209).then(n.bind(n,22832)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-4-the-shell-script-create-todo.md",22832],"47f57aeb":[()=>Promise.all([n.e(1869),n.e(9670)]).then(n.bind(n,82067)),"@site/docs/common/file-storage/upload-and-download-files.md",82067],"4846541e":[()=>n.e(7988).then(n.bind(n,14202)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/6-validation-and-sanitization.md",14202],"4849c7fa":[()=>n.e(5031).then(n.bind(n,46304)),"@site/versioned_docs/version-1.x/utilities/logging-and-debugging.md",46304],48640929:[()=>Promise.all([n.e(1869),n.e(9415)]).then(n.bind(n,46714)),"@site/blog/2022-02-13-version-2.8-release-notes.md",46714],"491c018d":[()=>n.e(3683).then(n.bind(n,81684)),"@site/versioned_docs/version-1.x/cookbook/scheduling-jobs.md",81684],"4a0a9e71":[()=>n.e(2113).then(n.bind(n,62676)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/2-database-set-up.md",62676],"4aba3dc4":[()=>n.e(5290).then(n.bind(n,39049)),"@site/docs/tutorials/real-world-example-with-react/5-our-first-route.md",39049],"4b13c2c2":[()=>n.e(8537).then(n.bind(n,22751)),"@site/versioned_docs/version-2.x/cookbook/scheduling-jobs.md",22751],"4c0d18a6":[()=>n.e(3346).then(n.bind(n,78441)),"@site/blog/2023-10-24-version-4.1-release-notes.md?truncated=true",78441],"4c77345c":[()=>Promise.all([n.e(1869),n.e(2263)]).then(n.bind(n,3509)),"@site/versioned_docs/version-2.x/upgrade-to-v2/file-upload-and-download.md",3509],"4c84a79a":[()=>n.e(242).then(n.bind(n,60786)),"@site/docs/community/awesome-foal.md",60786],"4cf056ae":[()=>n.e(6969).then(n.bind(n,60818)),"@site/blog/2021-12-12-version-2.7-release-notes.md?truncated=true",60818],"4db75e49":[()=>n.e(5969).then(n.bind(n,27907)),"@site/versioned_docs/version-1.x/development-environment/vscode.md",27907],"4e12f0a1":[()=>n.e(7939).then(n.bind(n,89576)),"@site/versioned_docs/version-1.x/cookbook/limit-repeated-requests.md",89576],"519173bc":[()=>n.e(3548).then(n.bind(n,23451)),"@site/docs/common/logging.md",23451],"52b298c9":[()=>n.e(1825).then(n.bind(n,30731)),"@site/docs/tutorials/simple-todo-list/6-validation-and-sanitization.md",30731],"52ca461d":[()=>Promise.all([n.e(1869),n.e(2547)]).then(n.bind(n,18623)),"@site/versioned_docs/version-2.x/file-system/upload-and-download-files.md",18623],"52f6d0d7":[()=>n.e(1177).then(n.bind(n,94521)),"@site/docs/frontend/nuxt.js.md",94521],"53e222b8":[()=>n.e(3924).then(n.bind(n,98794)),"@site/blog/2021-06-11-version-2.5-release-notes.md?truncated=true",98794],"5445446f":[()=>n.e(6381).then(n.bind(n,69941)),"@site/docs/authentication/user-class.md",69941],"554bc85a":[()=>n.e(372).then(n.bind(n,57946)),"@site/blog/2023-04-04-version-3.2-release-notes.md?truncated=true",57946],"567c169b":[()=>Promise.all([n.e(1869),n.e(1828)]).then(n.bind(n,17171)),"@site/docs/security/csrf-protection.md",17171],"56f23954":[()=>n.e(4245).then(n.bind(n,44982)),"@site/versioned_docs/version-3.x/databases/other-orm/introduction.md",44982],"571ed5e2":[()=>n.e(3149).then(n.bind(n,73554)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",73554],"5745e3db":[()=>n.e(7168).then(n.bind(n,9019)),"@site/versioned_docs/version-2.x/authentication-and-access-control/password-management.md",9019],"594157dd":[()=>n.e(4844).then(n.bind(n,18635)),"@site/versioned_docs/version-3.x/testing/unit-testing.md",18635],"596f5165":[()=>n.e(5512).then(n.bind(n,37657)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/4-the-shell-script-create-todo.md",37657],"5a184044":[()=>Promise.all([n.e(1869),n.e(9130)]).then(n.bind(n,68839)),"@site/blog/2021-04-08-whats-new-in-version-2-part-4.md?truncated=true",68839],"5a9147fc":[()=>n.e(5101).then(n.bind(n,6448)),"@site/versioned_docs/version-1.x/cloud/firebase.md",6448],"5af19d85":[()=>n.e(2712).then(n.bind(n,73290)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-1-Introduction.md",73290],"5b99ef51":[()=>n.e(1023).then(n.bind(n,87640)),"@site/versioned_docs/version-1.x/authentication-and-access-control/session-tokens.md",87640],"5bdb391c":[()=>Promise.all([n.e(1869),n.e(6276)]).then(n.bind(n,10041)),"@site/versioned_docs/version-3.x/common/file-storage/local-and-cloud-storage.md",10041],"5c06d4aa":[()=>n.e(5003).then(n.bind(n,34652)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/installation-troubleshooting.md",34652],"5c225a53":[()=>n.e(9621).then(n.bind(n,15223)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/7-add-frontend.md",15223],"5dde19ad":[()=>n.e(5524).then(n.t.bind(n,19592,19)),"~docs/default/version-3-x-metadata-prop-df2.json",19592],"5e2477ac":[()=>Promise.all([n.e(1869),n.e(8809)]).then(n.bind(n,81532)),"@site/versioned_docs/version-2.x/upgrade-to-v2/config-system.md",81532],"5e95c892":[()=>n.e(9647).then(n.bind(n,7121)),"@theme/DocsRoot",7121],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,4784)),"@generated/docusaurus.config",4784],"5ea367e5":[()=>n.e(42).then(n.bind(n,44101)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/1-installation.md",44101],"5eec65fd":[()=>n.e(8552).then(n.bind(n,57228)),"@site/versioned_docs/version-2.x/authentication-and-access-control/user-class.md",57228],"5ef0b58d":[()=>n.e(3264).then(n.bind(n,15308)),"@site/versioned_docs/version-3.x/frontend/not-found-page.md",15308],"611fda92":[()=>n.e(7001).then(n.bind(n,69532)),"@site/versioned_docs/version-2.x/architecture/hooks.md",69532],"616a0a7b":[()=>Promise.all([n.e(1869),n.e(7769)]).then(n.bind(n,56426)),"@site/docs/common/file-storage/local-and-cloud-storage.md",56426],"6186dfef":[()=>n.e(869).then(n.bind(n,96099)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/10-auth-with-react.md",96099],"61b34ffe":[()=>n.e(1940).then(n.bind(n,82926)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/1-installation.md",82926],"638a37f1":[()=>n.e(3830).then(n.bind(n,86037)),"@site/docs/cli/code-generation.md",86037],"63d06ba1":[()=>n.e(5528).then(n.bind(n,6508)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-2-introduction.md",6508],"644f641c":[()=>n.e(6753).then(n.bind(n,99422)),"@site/versioned_docs/version-2.x/api-section/graphql.md",99422],"6459326f":[()=>n.e(1484).then(n.t.bind(n,57757,19)),"/home/runner/work/foal/foal/docs/.docusaurus/docusaurus-theme-search-algolia/default/plugin-route-context-module-100.json",57757],"64fcdab6":[()=>n.e(833).then(n.bind(n,37591)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-1-installation.md",37591],"65ad1b4d":[()=>n.e(3778).then(n.bind(n,69870)),"@site/docs/common/serialization.md",69870],"65b85fac":[()=>n.e(5975).then(n.bind(n,28582)),"@site/docs/tutorials/real-world-example-with-react/12-file-upload.md",28582],"6653f08d":[()=>Promise.all([n.e(1869),n.e(4937)]).then(n.bind(n,10377)),"@site/versioned_docs/version-2.x/authentication-and-access-control/jwt.md",10377],"667eb670":[()=>n.e(1630).then(n.bind(n,96048)),"@site/versioned_docs/version-3.x/architecture/architecture-overview.md",96048],"66a5d301":[()=>n.e(3583).then(n.bind(n,49257)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/3-the-todo-model.md",49257],"67132fd6":[()=>n.e(8056).then(n.bind(n,27146)),"@site/versioned_docs/version-2.x/development-environment/code-generation.md",27146],"6744383d":[()=>n.e(7210).then(n.bind(n,15332)),"@site/blog/2021-04-22-version-2.3-release-notes.md?truncated=true",15332],"677578fe":[()=>n.e(640).then(n.bind(n,60439)),"@site/blog/2021-02-03-version-2.1-release-notes.md?truncated=true",60439],"67c30d44":[()=>n.e(68).then(n.bind(n,77320)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-2-the-user-and-todo-models.md",77320],"67e66c94":[()=>n.e(5517).then(n.bind(n,34284)),"@site/blog/2021-02-25-version-2.2-release-notes.md?truncated=true",34284],"6875c492":[()=>Promise.all([n.e(1869),n.e(8222),n.e(8544),n.e(4813)]).then(n.bind(n,33069)),"@theme/BlogTagsPostsPage",33069],"6a2d7719":[()=>n.e(1430).then(n.bind(n,61038)),"@site/docs/architecture/initialization.md",61038],"6a922744":[()=>n.e(2252).then(n.bind(n,38547)),"@site/docs/tutorials/real-world-example-with-react/6-swagger-interface.md",38547],"6acfb0dd":[()=>n.e(2077).then(n.bind(n,32993)),"@site/docs/common/async-tasks.md",32993],"6ada7a83":[()=>n.e(9882).then(n.bind(n,94243)),"@site/versioned_docs/version-1.x/api-section/graphql.md",94243],"6bc5bab8":[()=>n.e(5790).then(n.bind(n,236)),"@site/docs/tutorials/simple-todo-list/7-unit-testing.md",236],"6c012d97":[()=>n.e(501).then(n.bind(n,56818)),"@site/blog/2024-04-16-version-4.3-release-notes.md",56818],"6c2963dd":[()=>n.e(1784).then(n.bind(n,16294)),"@site/docs/tutorials/real-world-example-with-react/4-the-shell-scripts.md",16294],"6c6755eb":[()=>n.e(1892).then(n.bind(n,88886)),"@site/blog/2023-04-04-version-3.2-release-notes.md",88886],"6cfa5029":[()=>n.e(9557).then(n.bind(n,95750)),"@site/blog/2024-04-16-version-4.3-release-notes.md?truncated=true",95750],"6e6421c9":[()=>n.e(125).then(n.bind(n,78289)),"@site/versioned_docs/version-3.x/authentication/password-management.md",78289],"6ebe934b":[()=>n.e(1881).then(n.bind(n,95080)),"@site/docs/tutorials/simple-todo-list/installation-troubleshooting.md",95080],"6f11119e":[()=>n.e(7909).then(n.bind(n,59793)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/3-the-models.md",59793],"6f7dbe18":[()=>n.e(7662).then(n.bind(n,49810)),"@site/versioned_docs/version-2.x/upgrade-to-v2/service-and-app-initialization.md",49810],"7058414f":[()=>n.e(6546).then(n.bind(n,86146)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/12-file-upload.md",86146],"710a39fa":[()=>n.e(2649).then(n.bind(n,61575)),"@site/versioned_docs/version-1.x/security/http-headers-protection.md",61575],"7238c847":[()=>n.e(175).then(n.bind(n,65984)),"@site/versioned_docs/version-3.x/cli/commands.md",65984],"734aee64":[()=>Promise.all([n.e(1869),n.e(972)]).then(n.bind(n,77322)),"@site/docs/architecture/services-and-dependency-injection.md",77322],"73bfd16c":[()=>n.e(9525).then(n.t.bind(n,33935,19)),"~blog/default/blog-tags-release-page-2-ceb.json",33935],"750d9fcd":[()=>n.e(8523).then(n.bind(n,58866)),"@site/blog/2022-11-01-version-3.0-release-notes.md?truncated=true",58866],"75ab1baa":[()=>n.e(5885).then(n.bind(n,84985)),"@site/docs/deployment-and-environments/checklist.md",84985],"76972ae9":[()=>Promise.all([n.e(1869),n.e(5589)]).then(n.bind(n,29371)),"@site/blog/2021-03-11-whats-new-in-version-2-part-3.md?truncated=true",29371],"78c60ed3":[()=>n.e(1767).then(n.bind(n,48694)),"@site/docs/architecture/error-handling.md",48694],"7a2f366e":[()=>n.e(9639).then(n.bind(n,10212)),"@site/docs/databases/typeorm/create-models-and-queries.md",10212],"7a664337":[()=>n.e(6598).then(n.bind(n,52254)),"@site/versioned_docs/version-2.x/frontend-integration/nuxt.js.md",52254],"7a67b191":[()=>n.e(7599).then(n.bind(n,65384)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/5-the-rest-api.md",65384],"7b0c6911":[()=>n.e(9986).then(n.bind(n,85333)),"@site/versioned_docs/version-3.x/testing/e2e-testing.md",85333],"7c127e60":[()=>Promise.all([n.e(1869),n.e(1005)]).then(n.bind(n,78234)),"@site/versioned_docs/version-2.x/authentication-and-access-control/quick-start.md",78234],"7d1cfb7a":[()=>n.e(6486).then(n.bind(n,62254)),"@site/blog/2021-12-12-version-2.7-release-notes.md",62254],"7d8560c9":[()=>n.e(8017).then(n.bind(n,10312)),"@site/docs/security/http-headers-protection.md",10312],"7d8f027a":[()=>n.e(550).then(n.bind(n,5877)),"@site/versioned_docs/version-2.x/deployment-and-environments/checklist.md",5877],"7fb7d3c4":[()=>n.e(991).then(n.bind(n,52721)),"@site/docs/architecture/architecture-overview.md",52721],"7ffd8026":[()=>n.e(638).then(n.bind(n,76570)),"@site/versioned_docs/version-2.x/architecture/error-handling.md",76570],"80266b74":[()=>n.e(4954).then(n.bind(n,51851)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/5-the-rest-api.md",51851],"814f3328":[()=>n.e(7472).then(n.t.bind(n,55513,19)),"~blog/default/blog-post-list-prop-default.json",55513],"81cd6784":[()=>n.e(3538).then(n.bind(n,33169)),"@site/versioned_docs/version-3.x/architecture/hooks.md",33169],"834b034e":[()=>n.e(5768).then(n.bind(n,69590)),"@site/versioned_docs/version-2.x/common/generate-tokens.md",69590],"83d480e9":[()=>n.e(9650).then(n.t.bind(n,44078,19)),"~blog/default/blog-tags-release-b5c.json",44078],"854d3434":[()=>Promise.all([n.e(1869),n.e(6674)]).then(n.bind(n,38289)),"@site/versioned_docs/version-2.x/api-section/openapi-and-swagger-ui.md",38289],"855cb55f":[()=>n.e(2324).then(n.bind(n,91897)),"@site/versioned_docs/version-2.x/upgrade-to-v2/cli-commands.md",91897],"857cb1d3":[()=>Promise.all([n.e(1869),n.e(8800)]).then(n.bind(n,79789)),"@site/docs/common/validation-and-sanitization.md",79789],"8636e38a":[()=>n.e(423).then(n.bind(n,70843)),"@site/versioned_docs/version-1.x/cookbook/request-body-size.md",70843],"8637c6fb":[()=>n.e(7052).then(n.bind(n,31276)),"@site/versioned_docs/version-2.x/authentication-and-access-control/administrators-and-roles.md",31276],"869c4885":[()=>n.e(3876).then(n.bind(n,91212)),"@site/versioned_docs/version-2.x/websockets.md",91212],"86f9eebb":[()=>n.e(6676).then(n.bind(n,3708)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-7-the-signup-page.md",3708],"88489f42":[()=>Promise.all([n.e(1869),n.e(2944)]).then(n.bind(n,95035)),"@site/versioned_docs/version-3.x/architecture/services-and-dependency-injection.md",95035],"887c1a48":[()=>n.e(8471).then(n.bind(n,51656)),"@site/versioned_docs/version-1.x/development-environment/build-and-start-the-app.md",51656],"88a145c0":[()=>n.e(485).then(n.bind(n,51619)),"@site/versioned_docs/version-3.x/architecture/configuration.md",51619],"88e32be2":[()=>n.e(2622).then(n.bind(n,21036)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/14-production-build.md",21036],"88e812ba":[()=>n.e(4738).then(n.bind(n,81713)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/4-the-shell-scripts.md",81713],"89e75e11":[()=>n.e(182).then(n.bind(n,59733)),"@site/docs/common/expressjs.md",59733],"89e760d9":[()=>n.e(3557).then(n.bind(n,57997)),"@site/versioned_docs/version-2.x/development-environment/linting-and-code-style.md",57997],"89f16618":[()=>n.e(9834).then(n.bind(n,36085)),"@site/blog/2021-02-03-version-2.1-release-notes.md",36085],"8cecbefb":[()=>n.e(8463).then(n.bind(n,63968)),"@site/blog/2021-02-25-version-2.2-release-notes.md",63968],"8d974a0f":[()=>n.e(6708).then(n.bind(n,72905)),"@site/versioned_docs/version-1.x/architecture/services-and-dependency-injection.md",72905],"8e3732cc":[()=>n.e(6933).then(n.bind(n,67324)),"@site/docs/security/cors.md",67324],"8eb4e46b":[()=>n.e(5767).then(n.t.bind(n,20541,19)),"~blog/default/blog-page-2-677.json",20541],"8f1b2eb6":[()=>n.e(659).then(n.bind(n,65034)),"@site/docs/architecture/configuration.md",65034],"8f4eeb12":[()=>n.e(2496).then(n.bind(n,5366)),"@site/blog/2021-05-19-version-2.4-release-notes.md",5366],"8f84b176":[()=>n.e(3681).then(n.bind(n,55777)),"@site/versioned_docs/version-1.x/cookbook/error-handling.md",55777],"902c734f":[()=>Promise.all([n.e(1869),n.e(7725)]).then(n.bind(n,30438)),"@site/docs/authentication/jwt.md",30438],"9183ea35":[()=>n.e(152).then(n.bind(n,80611)),"@site/versioned_docs/version-1.x/authentication-and-access-control/administrators-and-roles.md",80611],"91ec3cab":[()=>n.e(2006).then(n.bind(n,24727)),"@site/versioned_docs/version-3.x/community/awesome-foal.md",24727],"92999a1c":[()=>n.e(8790).then(n.t.bind(n,81116,19)),"~blog/default/blog-page-3-fd4.json",81116],"9326e8ff":[()=>n.e(8208).then(n.bind(n,90043)),"@site/docs/tutorials/real-world-example-with-react/13-csrf.md",90043],93311995:[()=>Promise.all([n.e(1869),n.e(3747)]).then(n.bind(n,37176)),"@site/blog/2021-02-17-whats-new-in-version-2-part-1.md",37176],"935f2afb":[()=>n.e(8581).then(n.t.bind(n,35610,19)),"~docs/default/version-current-metadata-prop-751.json",35610],"93b3e974":[()=>n.e(565).then(n.bind(n,40011)),"@site/versioned_docs/version-3.x/databases/typeorm/introduction.md",40011],"93bf9c1e":[()=>n.e(6869).then(n.bind(n,71908)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo.md",71908],"96bb02d9":[()=>n.e(7681).then(n.bind(n,29908)),"@site/versioned_docs/version-3.x/frontend/single-page-applications.md",29908],97315902:[()=>n.e(5107).then(n.t.bind(n,52945,19)),"/home/runner/work/foal/foal/docs/.docusaurus/docusaurus-plugin-content-blog/default/plugin-route-context-module-100.json",52945],"978f2691":[()=>n.e(8983).then(n.bind(n,52930)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/3-the-todo-model.md",52930],"97f79129":[()=>n.e(3227).then(n.bind(n,22955)),"@site/versioned_docs/version-3.x/common/utilities.md",22955],"996b691f":[()=>n.e(4343).then(n.bind(n,60625)),"@site/versioned_docs/version-3.x/architecture/initialization.md",60625],"9a121f14":[()=>n.e(9238).then(n.bind(n,81175)),"@site/versioned_docs/version-3.x/common/websockets.md",81175],"9a2e213c":[()=>Promise.all([n.e(1869),n.e(8295)]).then(n.bind(n,67813)),"@site/blog/2021-04-08-whats-new-in-version-2-part-4.md",67813],"9a3a9a77":[()=>Promise.all([n.e(1869),n.e(1237)]).then(n.bind(n,68281)),"@site/versioned_docs/version-2.x/common/templating.md",68281],"9b1d1d1a":[()=>Promise.all([n.e(1869),n.e(1256)]).then(n.bind(n,35010)),"@site/versioned_docs/version-3.x/security/csrf-protection.md",35010],"9c021584":[()=>n.e(1307).then(n.t.bind(n,5173,19)),"~blog/default/blog-tags-release-b5c-list.json",5173],"9ca94865":[()=>n.e(2674).then(n.bind(n,26629)),"@site/docs/tutorials/simple-todo-list/3-the-todo-model.md",26629],"9dd8a0d2":[()=>Promise.all([n.e(1869),n.e(8617)]).then(n.bind(n,17630)),"@site/src/pages/index.jsx",17630],"9e4087bc":[()=>n.e(2711).then(n.bind(n,89331)),"@theme/BlogArchivePage",89331],"9fdcc880":[()=>n.e(9396).then(n.bind(n,73031)),"@site/docs/databases/other-orm/prisma.md",73031],a0071fee:[()=>n.e(2368).then(n.bind(n,89383)),"@site/versioned_docs/version-1.x/api-section/rest-blueprints.md",89383],a0106e89:[()=>n.e(5949).then(n.bind(n,39876)),"@site/versioned_docs/version-3.x/security/rate-limiting.md",39876],a15998e5:[()=>n.e(750).then(n.bind(n,78379)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/2-database-set-up.md",78379],a1691299:[()=>n.e(3581).then(n.t.bind(n,4061,19)),"/home/runner/work/foal/foal/docs/.docusaurus/docusaurus-plugin-content-pages/default/plugin-route-context-module-100.json",4061],a1ccd797:[()=>n.e(9760).then(n.bind(n,47959)),"@site/blog/2023-08-13-version-3.3-release-notes.md?truncated=true",47959],a252a406:[()=>n.e(3766).then(n.bind(n,83788)),"@site/docs/common/websockets.md",83788],a28fa369:[()=>n.e(1651).then(n.bind(n,11891)),"@site/versioned_docs/version-1.x/file-system/local-and-cloud-storage.md",11891],a31c6fda:[()=>n.e(8618).then(n.bind(n,1500)),"@site/versioned_docs/version-1.x/authentication-and-access-control/jwt.md",1500],a3f09207:[()=>n.e(4259).then(n.bind(n,84027)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-3-the-shell-scripts.md",84027],a4a84f9e:[()=>n.e(6275).then(n.bind(n,43156)),"@site/versioned_docs/version-1.x/development-environment/create-and-run-scripts.md",43156],a57a69a0:[()=>Promise.all([n.e(1869),n.e(1773)]).then(n.bind(n,2216)),"@site/versioned_docs/version-3.x/common/validation-and-sanitization.md",2216],a591f281:[()=>n.e(9514).then(n.bind(n,60938)),"@site/versioned_docs/version-3.x/authentication/user-class.md",60938],a6961750:[()=>n.e(9880).then(n.bind(n,51491)),"@site/versioned_docs/version-1.x/testing/e2e-testing.md",51491],a6aa9e1f:[()=>Promise.all([n.e(1869),n.e(8222),n.e(8544),n.e(7643)]).then(n.bind(n,77785)),"@theme/BlogListPage",77785],a7023ddc:[()=>n.e(9267).then(n.t.bind(n,28289,19)),"~blog/default/blog-tags-tags-4c2.json",28289],a74ed5d5:[()=>n.e(9162).then(n.bind(n,22779)),"@site/blog/2024-08-22-version-4.5-release-notes.md",22779],a761f982:[()=>n.e(701).then(n.bind(n,35318)),"@site/blog/2023-09-11-version-4.0-release-notes.md?truncated=true",35318],a7bd4aaa:[()=>n.e(7098).then(n.bind(n,74532)),"@theme/DocVersionRoot",74532],a7d0b318:[()=>n.e(1841).then(n.bind(n,30814)),"@site/versioned_docs/version-3.x/deployment-and-environments/checklist.md",30814],a89f7d55:[()=>n.e(2026).then(n.bind(n,39673)),"@site/versioned_docs/version-3.x/security/http-headers-protection.md",39673],a8c77290:[()=>Promise.all([n.e(1869),n.e(38)]).then(n.bind(n,81127)),"@site/versioned_docs/version-3.x/authentication/jwt.md",81127],a8e5e6db:[()=>n.e(3093).then(n.bind(n,1555)),"@site/versioned_docs/version-1.x/utilities/templating.md",1555],a93a1ece:[()=>n.e(7127).then(n.bind(n,82669)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-5-auth-controllers-and-hooks.md",82669],a94703ab:[()=>Promise.all([n.e(1869),n.e(9048)]).then(n.bind(n,92559)),"@theme/DocRoot",92559],ab3343fd:[()=>n.e(8149).then(n.bind(n,35936)),"@site/versioned_docs/version-1.x/cloud/aws-beanstalk.md",35936],ab721ce5:[()=>n.e(9191).then(n.bind(n,2290)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/2-introduction.md",2290],ad0d29b1:[()=>n.e(2072).then(n.bind(n,67834)),"@site/docs/frontend/angular-react-vue.md",67834],ad438b77:[()=>n.e(2775).then(n.bind(n,88823)),"@site/blog/2023-10-24-version-4.1-release-notes.md",88823],ad72b598:[()=>n.e(9603).then(n.bind(n,1942)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/7-add-frontend.md",1942],ad8f4434:[()=>Promise.all([n.e(1869),n.e(2607)]).then(n.bind(n,77459)),"@site/versioned_docs/version-2.x/upgrade-to-v2/session-tokens.md",77459],add410f2:[()=>n.e(5271).then(n.bind(n,27920)),"@site/docs/common/gRPC.md",27920],ae72009b:[()=>n.e(5755).then(n.bind(n,74550)),"@site/versioned_docs/version-2.x/frontend-integration/single-page-applications.md",74550],aef2f35b:[()=>n.e(6365).then(n.bind(n,21605)),"@site/docs/cli/commands.md",21605],af8b5c27:[()=>n.e(3117).then(n.bind(n,6278)),"@site/versioned_docs/version-2.x/architecture/configuration.md",6278],afa51b52:[()=>n.e(3720).then(n.bind(n,60584)),"@site/versioned_docs/version-3.x/common/expressjs.md",60584],afbbe19c:[()=>n.e(4124).then(n.bind(n,87297)),"@site/versioned_docs/version-3.x/tutorials/simple-todo-list/2-introduction.md",87297],afea1f01:[()=>n.e(172).then(n.bind(n,776)),"@site/versioned_docs/version-2.x/development-environment/vscode.md",776],b03290eb:[()=>Promise.all([n.e(1869),n.e(80)]).then(n.bind(n,23035)),"@site/docs/authentication/social-auth.md",23035],b26bf12b:[()=>n.e(4776).then(n.bind(n,44777)),"@site/versioned_docs/version-1.x/frontend-integration/nuxt.js.md",44777],b2b675dd:[()=>n.e(1991).then(n.t.bind(n,29775,19)),"~blog/default/blog-c06.json",29775],b2f554cd:[()=>n.e(5894).then(n.t.bind(n,76042,19)),"~blog/default/blog-archive-80c.json",76042],b31df0b0:[()=>n.e(1225).then(n.t.bind(n,13069,19)),"~blog/default/blog-tags-release-page-3-4c9-list.json",13069],b47c3189:[()=>n.e(7442).then(n.bind(n,99046)),"@site/versioned_docs/version-3.x/databases/other-orm/prisma.md",99046],b53ea245:[()=>n.e(3868).then(n.bind(n,66197)),"@site/versioned_docs/version-3.x/cli/linting-and-code-style.md",66197],b5c05500:[()=>n.e(63).then(n.bind(n,85327)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/14-production-build.md",85327],b686b0f5:[()=>n.e(9132).then(n.bind(n,77211)),"@site/versioned_docs/version-3.x/cli/shell-scripts.md",77211],b8f1ba86:[()=>n.e(5331).then(n.bind(n,64875)),"@site/versioned_docs/version-2.x/cookbook/root-imports.md",64875],b94e11fc:[()=>n.e(4998).then(n.bind(n,66235)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/8-authentication.md",66235],b9a3e93e:[()=>Promise.all([n.e(1869),n.e(2626)]).then(n.bind(n,11082)),"@site/versioned_docs/version-3.x/common/logging.md",11082],b9bf7414:[()=>n.e(4494).then(n.bind(n,94943)),"@site/versioned_docs/version-1.x/authentication-and-access-control/quick-start.md",94943],ba300e46:[()=>n.e(2426).then(n.bind(n,77685)),"@site/versioned_docs/version-1.x/cookbook/not-found-page.md",77685],bc78cc67:[()=>n.e(4312).then(n.bind(n,21734)),"@site/versioned_docs/version-2.x/upgrade-to-v2/application-creation.md",21734],bcb63fa7:[()=>n.e(9333).then(n.bind(n,44613)),"@site/blog/2022-08-11-version-2.10-release-notes.md",44613],bd0c6f53:[()=>n.e(8110).then(n.bind(n,89679)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/11-sign-up.md",89679],bd8a10bb:[()=>n.e(3643).then(n.bind(n,11698)),"@site/versioned_docs/version-1.x/databases/typeorm.md",11698],bd8f4650:[()=>n.e(1262).then(n.bind(n,72929)),"@site/docs/tutorials/simple-todo-list/1-installation.md",72929],c1bfbf8b:[()=>Promise.all([n.e(1869),n.e(8857)]).then(n.bind(n,66788)),"@site/blog/2021-02-17-whats-new-in-version-2-part-1.md?truncated=true",66788],c238c009:[()=>n.e(7537).then(n.bind(n,1720)),"@site/blog/2021-04-22-version-2.3-release-notes.md",1720],c2d0cba9:[()=>n.e(8141).then(n.bind(n,75606)),"@site/versioned_docs/version-3.x/common/graphql.md",75606],c33a3301:[()=>n.e(4282).then(n.bind(n,48912)),"@site/blog/2022-10-09-version-2.11-release-notes.md",48912],c3fb1f12:[()=>n.e(8477).then(n.bind(n,85839)),"@site/versioned_docs/version-2.x/authentication-and-access-control/groups-and-permissions.md",85839],c4282154:[()=>n.e(9308).then(n.bind(n,90077)),"@site/versioned_docs/version-2.x/architecture/architecture-overview.md",90077],c49326f7:[()=>n.e(1447).then(n.bind(n,65406)),"@site/docs/tutorials/real-world-example-with-react/9-authenticated-api.md",65406],c4be288c:[()=>Promise.all([n.e(1869),n.e(940)]).then(n.bind(n,25136)),"@site/versioned_docs/version-3.x/authentication/quick-start.md",25136],c4ee04fe:[()=>n.e(2131).then(n.bind(n,38748)),"@site/versioned_docs/version-1.x/databases/mongodb.md",38748],c55dc650:[()=>n.e(3356).then(n.bind(n,98887)),"@site/versioned_docs/version-1.x/architecture/hooks.md",98887],c5ab5773:[()=>n.e(7096).then(n.bind(n,1714)),"@site/versioned_docs/version-2.x/architecture/initialization.md",1714],c72e1ae8:[()=>n.e(6507).then(n.bind(n,91981)),"@site/versioned_docs/version-3.x/authorization/administrators-and-roles.md",91981],c7b9c9de:[()=>n.e(2075).then(n.bind(n,70570)),"@site/versioned_docs/version-1.x/authentication-and-access-control/password-management.md",70570],c7c1656f:[()=>n.e(5426).then(n.bind(n,22847)),"@site/docs/tutorials/real-world-example-with-react/8-authentication.md",22847],c7ca52f5:[()=>n.e(4299).then(n.bind(n,96501)),"@site/versioned_docs/version-1.x/authentication-and-access-control/social-auth.md",96501],c8185609:[()=>n.e(6329).then(n.bind(n,40294)),"@site/versioned_docs/version-1.x/development-environment/linting-and-code-style.md",40294],caa9b0f2:[()=>Promise.all([n.e(1869),n.e(6903)]).then(n.bind(n,47223)),"@site/versioned_docs/version-2.x/authentication-and-access-control/session-tokens.md",47223],cb8922cc:[()=>n.e(9531).then(n.bind(n,73672)),"@site/versioned_docs/version-3.x/upgrade-to-v3/README.md",73672],cb94d7af:[()=>n.e(9249).then(n.bind(n,79239)),"@site/blog/2022-08-11-version-2.10-release-notes.md?truncated=true",79239],cbe1eddd:[()=>Promise.all([n.e(1869),n.e(6017)]).then(n.bind(n,22037)),"@site/docs/comparison-with-other-frameworks/express-fastify.md",22037],ccb48262:[()=>n.e(4593).then(n.bind(n,22170)),"@site/versioned_docs/version-2.x/upgrade-to-v2/error-handling.md",22170],ccc49370:[()=>Promise.all([n.e(1869),n.e(8222),n.e(8544),n.e(3249)]).then(n.bind(n,84029)),"@theme/BlogPostPage",84029],cd135096:[()=>Promise.all([n.e(1869),n.e(1608)]).then(n.bind(n,74949)),"@site/versioned_docs/version-3.x/authentication/session-tokens.md",74949],cd9f68e2:[()=>Promise.all([n.e(1869),n.e(9904)]).then(n.bind(n,33838)),"@site/blog/2021-03-02-whats-new-in-version-2-part-2.md",33838],cdc85a21:[()=>n.e(3918).then(n.bind(n,85335)),"@site/docs/tutorials/real-world-example-with-react/10-auth-with-react.md",85335],cdd202a9:[()=>n.e(2668).then(n.bind(n,26751)),"@site/versioned_docs/version-1.x/frontend-integration/jsx-server-side-rendering.md",26751],ce335d8a:[()=>n.e(7975).then(n.bind(n,32318)),"@site/versioned_docs/version-3.x/authorization/groups-and-permissions.md",32318],cf304687:[()=>n.e(2856).then(n.bind(n,87552)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/6-swagger-interface.md",87552],cfd77fea:[()=>n.e(7670).then(n.bind(n,75795)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/15-social-auth.md",75795],d0f68168:[()=>n.e(4896).then(n.bind(n,18706)),"@site/versioned_docs/version-2.x/upgrade-to-v2/mongodb.md",18706],d1a6e407:[()=>n.e(5727).then(n.bind(n,19714)),"@site/versioned_docs/version-2.x/common/serializing-and-deserializing.md",19714],d28f4439:[()=>n.e(3219).then(n.bind(n,23471)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/6-swagger-interface.md",23471],d465be9c:[()=>n.e(1487).then(n.bind(n,4168)),"@site/blog/2021-09-19-version-2.6-release-notes.md",4168],d4e894f9:[()=>n.e(992).then(n.bind(n,12727)),"@site/versioned_docs/version-2.x/databases/create-models-and-queries.md",12727],d5913837:[()=>n.e(7823).then(n.bind(n,14989)),"@site/blog/2024-08-22-version-4.5-release-notes.md?truncated=true",14989],d63fe0c7:[()=>n.e(8871).then(n.bind(n,64691)),"@site/versioned_docs/version-1.x/frontend-integration/single-page-applications.md",64691],d7767d70:[()=>Promise.all([n.e(1869),n.e(6491)]).then(n.bind(n,55066)),"@site/docs/authentication/session-tokens.md",55066],d79d4ecc:[()=>Promise.all([n.e(1869),n.e(7540)]).then(n.bind(n,28047)),"@site/versioned_docs/version-2.x/upgrade-to-v2/template-engine.md",28047],d8400b9d:[()=>n.e(2998).then(n.bind(n,69984)),"@site/docs/authorization/administrators-and-roles.md",69984],d93887b0:[()=>n.e(302).then(n.bind(n,8265)),"@site/versioned_docs/version-1.x/cookbook/generate-tokens.md",8265],d97194cc:[()=>n.e(8587).then(n.bind(n,82786)),"@site/versioned_docs/version-1.x/architecture/architecture-overview.md",82786],d9855914:[()=>n.e(1082).then(n.bind(n,72424)),"@site/versioned_docs/version-1.x/security/csrf-protection.md",72424],d9c83d6c:[()=>n.e(639).then(n.bind(n,88063)),"@site/docs/databases/other-orm/introduction.md",88063],da411a71:[()=>Promise.all([n.e(1869),n.e(9081)]).then(n.bind(n,88461)),"@site/docs/security/body-size-limiting.md",88461],daacca3b:[()=>n.e(8389).then(n.bind(n,22816)),"@site/docs/tutorials/real-world-example-with-react/14-production-build.md",22816],dbced382:[()=>n.e(5656).then(n.bind(n,36987)),"@site/versioned_docs/version-1.x/authentication-and-access-control/user-class.md",36987],dbf0f076:[()=>n.e(8922).then(n.bind(n,72092)),"@site/blog/2022-10-09-version-2.11-release-notes.md?truncated=true",72092],dcd6df2d:[()=>n.e(5914).then(n.bind(n,52139)),"@site/blog/2024-04-25-version-4.4-release-notes.md?truncated=true",52139],dd6459f3:[()=>n.e(4466).then(n.bind(n,59491)),"@site/docs/common/rest-blueprints.md",59491],ddc5ebcf:[()=>n.e(9918).then(n.bind(n,75349)),"@site/versioned_docs/version-3.x/architecture/error-handling.md",75349],df60c465:[()=>n.e(8013).then(n.bind(n,61544)),"@site/versioned_docs/version-1.x/authentication-and-access-control/groups-and-permissions.md",61544],df855cfd:[()=>n.e(8386).then(n.bind(n,93329)),"@site/versioned_docs/version-2.x/testing/introduction.md",93329],dfa4835c:[()=>n.e(1621).then(n.bind(n,64501)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/5-our-first-route.md",64501],e0c5964b:[()=>n.e(8095).then(n.bind(n,62485)),"@site/versioned_docs/version-3.x/common/task-scheduling.md",62485],e141f46d:[()=>n.e(268).then(n.bind(n,66920)),"@site/versioned_docs/version-1.x/tutorials/multi-user-todo-list/tuto-6-todos-and-ownership.md",66920],e238a5d7:[()=>n.e(8202).then(n.bind(n,1e3)),"@site/versioned_docs/version-3.x/architecture/controllers.md",1e3],e2a8b2ab:[()=>n.e(1408).then(n.bind(n,10732)),"@site/docs/tutorials/real-world-example-with-react/3-the-models.md",10732],e3e9f084:[()=>n.e(9503).then(n.bind(n,94523)),"@site/versioned_docs/version-3.x/frontend/angular-react-vue.md",94523],e3ec4ccc:[()=>Promise.all([n.e(1869),n.e(3658)]).then(n.bind(n,28105)),"@site/docs/authentication/quick-start.md",28105],e533cdd5:[()=>n.e(1695).then(n.bind(n,49996)),"@site/versioned_docs/version-2.x/security/http-headers-protection.md",49996],e5d3578a:[()=>n.e(8175).then(n.bind(n,51407)),"@site/versioned_docs/version-2.x/tutorials/simple-todo-list/6-validation-and-sanitization.md",51407],e80c6fff:[()=>n.e(1779).then(n.bind(n,56235)),"@site/versioned_docs/version-1.x/serializing-and-deserializing.md",56235],e825831c:[()=>Promise.all([n.e(1869),n.e(5160)]).then(n.bind(n,61084)),"@site/versioned_docs/version-3.x/comparison-with-other-frameworks/express-fastify.md",61084],e857607a:[()=>n.e(965).then(n.bind(n,21820)),"@site/docs/cli/shell-scripts.md",21820],e88bd2ea:[()=>n.e(3738).then(n.bind(n,67574)),"@site/versioned_docs/version-3.x/tutorials/real-world-example-with-react/15-social-auth.md",67574],e8a5a4d0:[()=>n.e(9161).then(n.bind(n,18815)),"@site/versioned_docs/version-3.x/databases/typeorm/create-models-and-queries.md",18815],e9534d0a:[()=>n.e(6512).then(n.bind(n,68698)),"@site/blog/2021-05-19-version-2.4-release-notes.md?truncated=true",68698],ea0706a6:[()=>n.e(6245).then(n.bind(n,73341)),"@site/versioned_docs/version-1.x/databases/using-another-orm.md",73341],ea5ed2c7:[()=>n.e(7887).then(n.bind(n,5584)),"@site/versioned_docs/version-2.x/upgrade-to-v2/README.md",5584],eb160070:[()=>n.e(4914).then(n.bind(n,6170)),"@site/versioned_docs/version-3.x/common/rest-blueprints.md",6170],eb299cb3:[()=>n.e(3280).then(n.t.bind(n,23422,19)),"~blog/default/blog-tags-release-page-2-ceb-list.json",23422],ed1eff10:[()=>n.e(305).then(n.bind(n,77781)),"@site/src/pages/who-is-using-foal.jsx",77781],ee538668:[()=>Promise.all([n.e(1869),n.e(6597)]).then(n.bind(n,24120)),"@site/versioned_docs/version-2.x/authentication-and-access-control/social-auth.md",24120],efcad0c3:[()=>n.e(1376).then(n.bind(n,60867)),"@site/docs/frontend/single-page-applications.md",60867],f1d65ad8:[()=>n.e(9436).then(n.bind(n,16704)),"@site/versioned_docs/version-3.x/frontend/nuxt.js.md",16704],f2916434:[()=>n.e(6618).then(n.bind(n,84201)),"@site/versioned_docs/version-1.x/tutorials/mongodb-todo-list/tuto-6-validation-and-sanitization.md",84201],f31b991d:[()=>n.e(5710).then(n.bind(n,10338)),"@site/versioned_docs/version-2.x/api-section/rest-blueprints.md",10338],f35b057c:[()=>n.e(1012).then(n.bind(n,40914)),"@site/versioned_docs/version-1.x/cookbook/expressjs.md",40914],f5b890ba:[()=>n.e(998).then(n.t.bind(n,91518,19)),"~blog/default/blog-tags-survey-f63.json",91518],f62baa69:[()=>Promise.all([n.e(1869),n.e(98)]).then(n.bind(n,59238)),"@site/versioned_docs/version-2.x/common/logging-and-debugging.md",59238],f767b076:[()=>n.e(4074).then(n.bind(n,99428)),"@site/versioned_docs/version-2.x/frontend-integration/jsx-server-side-rendering.md",99428],f819756d:[()=>n.e(2521).then(n.bind(n,66135)),"@site/docs/tutorials/real-world-example-with-react/15-social-auth.md",66135],f85a1a6c:[()=>n.e(6244).then(n.bind(n,21926)),"@site/docs/testing/unit-testing.md",21926],f8ed6dc4:[()=>n.e(4541).then(n.bind(n,29363)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-3-the-todo-model.md",29363],fadb23e8:[()=>Promise.all([n.e(1869),n.e(540)]).then(n.bind(n,90702)),"@site/versioned_docs/version-2.x/file-system/local-and-cloud-storage.md",90702],fbfc241e:[()=>n.e(1410).then(n.bind(n,93733)),"@site/versioned_docs/version-1.x/tutorials/simple-todo-list/tuto-6-validation-and-sanitization.md",93733],ffbba8e2:[()=>n.e(4793).then(n.bind(n,9266)),"@site/versioned_docs/version-2.x/tutorials/real-world-example-with-react/11-sign-up.md",9266]};var s=n(74848);function l(e){let{error:t,retry:n,pastDelay:o}=e;return t?(0,s.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,s.jsx)("p",{children:String(t)}),(0,s.jsx)("div",{children:(0,s.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):o?(0,s.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,s.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,s.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,s.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var c=n(86921),d=n(53102);function u(e,t){if("*"===e)return r()({loading:l,loader:()=>n.e(2237).then(n.bind(n,82237)),modules:["@theme/NotFound"],webpack:()=>[82237],render(e,t){const n=e.default;return(0,s.jsx)(d.W,{value:{plugin:{name:"native",id:"default"}},children:(0,s.jsx)(n,{...t})})}});const o=a[`${e}-${t}`],u={},p=[],m=[],f=(0,c.A)(o);return Object.entries(f).forEach((e=>{let[t,n]=e;const o=i[n];o&&(u[t]=o[0],p.push(o[1]),m.push(o[2]))})),r().Map({loading:l,loader:u,modules:p,webpack:()=>m,render(t,n){const r=JSON.parse(JSON.stringify(o));Object.entries(t).forEach((t=>{let[n,o]=t;const a=o.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(o).filter((e=>"default"!==e)).forEach((e=>{a[e]=o[e]}));let i=r;const s=n.split(".");s.slice(0,-1).forEach((e=>{i=i[e]})),i[s[s.length-1]]=a}));const a=r.__comp;delete r.__comp;const i=r.__context;return delete r.__context,(0,s.jsx)(d.W,{value:i,children:(0,s.jsx)(a,{...r,...n})})}})}const p=[{path:"/blog",component:u("/blog","51f"),exact:!0},{path:"/blog/2021/02/03/version-2.1-release-notes",component:u("/blog/2021/02/03/version-2.1-release-notes","069"),exact:!0},{path:"/blog/2021/02/17/whats-new-in-version-2-part-1",component:u("/blog/2021/02/17/whats-new-in-version-2-part-1","06b"),exact:!0},{path:"/blog/2021/02/25/version-2.2-release-notes",component:u("/blog/2021/02/25/version-2.2-release-notes","d27"),exact:!0},{path:"/blog/2021/03/02/whats-new-in-version-2-part-2",component:u("/blog/2021/03/02/whats-new-in-version-2-part-2","ad9"),exact:!0},{path:"/blog/2021/03/11/whats-new-in-version-2-part-3",component:u("/blog/2021/03/11/whats-new-in-version-2-part-3","c39"),exact:!0},{path:"/blog/2021/04/08/whats-new-in-version-2-part-4",component:u("/blog/2021/04/08/whats-new-in-version-2-part-4","f66"),exact:!0},{path:"/blog/2021/04/22/version-2.3-release-notes",component:u("/blog/2021/04/22/version-2.3-release-notes","714"),exact:!0},{path:"/blog/2021/05/19/version-2.4-release-notes",component:u("/blog/2021/05/19/version-2.4-release-notes","ecb"),exact:!0},{path:"/blog/2021/06/11/version-2.5-release-notes",component:u("/blog/2021/06/11/version-2.5-release-notes","8e1"),exact:!0},{path:"/blog/2021/09/19/version-2.6-release-notes",component:u("/blog/2021/09/19/version-2.6-release-notes","382"),exact:!0},{path:"/blog/2021/12/12/version-2.7-release-notes",component:u("/blog/2021/12/12/version-2.7-release-notes","139"),exact:!0},{path:"/blog/2022/02/13/version-2.8-release-notes",component:u("/blog/2022/02/13/version-2.8-release-notes","aff"),exact:!0},{path:"/blog/2022/05/29/version-2.9-release-notes",component:u("/blog/2022/05/29/version-2.9-release-notes","31b"),exact:!0},{path:"/blog/2022/06/13/FoalTS-2022-survey-is-open",component:u("/blog/2022/06/13/FoalTS-2022-survey-is-open","bf0"),exact:!0},{path:"/blog/2022/08/11/version-2.10-release-notes",component:u("/blog/2022/08/11/version-2.10-release-notes","ed3"),exact:!0},{path:"/blog/2022/10/09/version-2.11-release-notes",component:u("/blog/2022/10/09/version-2.11-release-notes","d02"),exact:!0},{path:"/blog/2022/11/01/version-3.0-release-notes",component:u("/blog/2022/11/01/version-3.0-release-notes","dc2"),exact:!0},{path:"/blog/2022/11/28/version-3.1-release-notes",component:u("/blog/2022/11/28/version-3.1-release-notes","5c4"),exact:!0},{path:"/blog/2023/04/04/version-3.2-release-notes",component:u("/blog/2023/04/04/version-3.2-release-notes","d00"),exact:!0},{path:"/blog/2023/08/13/version-3.3-release-notes",component:u("/blog/2023/08/13/version-3.3-release-notes","2af"),exact:!0},{path:"/blog/2023/09/11/version-4.0-release-notes",component:u("/blog/2023/09/11/version-4.0-release-notes","ced"),exact:!0},{path:"/blog/2023/10/24/version-4.1-release-notes",component:u("/blog/2023/10/24/version-4.1-release-notes","ed2"),exact:!0},{path:"/blog/2023/10/29/version-4.2-release-notes",component:u("/blog/2023/10/29/version-4.2-release-notes","6e1"),exact:!0},{path:"/blog/2024/04/16/version-4.3-release-notes",component:u("/blog/2024/04/16/version-4.3-release-notes","914"),exact:!0},{path:"/blog/2024/04/25/version-4.4-release-notes",component:u("/blog/2024/04/25/version-4.4-release-notes","936"),exact:!0},{path:"/blog/2024/08/22/version-4.5-release-notes",component:u("/blog/2024/08/22/version-4.5-release-notes","558"),exact:!0},{path:"/blog/archive",component:u("/blog/archive","fee"),exact:!0},{path:"/blog/page/2",component:u("/blog/page/2","030"),exact:!0},{path:"/blog/page/3",component:u("/blog/page/3","a07"),exact:!0},{path:"/blog/tags",component:u("/blog/tags","e17"),exact:!0},{path:"/blog/tags/release",component:u("/blog/tags/release","5d0"),exact:!0},{path:"/blog/tags/release/page/2",component:u("/blog/tags/release/page/2","dcc"),exact:!0},{path:"/blog/tags/release/page/3",component:u("/blog/tags/release/page/3","28c"),exact:!0},{path:"/blog/tags/survey",component:u("/blog/tags/survey","e8a"),exact:!0},{path:"/newsletter",component:u("/newsletter","ca0"),exact:!0},{path:"/search",component:u("/search","677"),exact:!0},{path:"/who-is-using-foal",component:u("/who-is-using-foal","5a5"),exact:!0},{path:"/docs",component:u("/docs","b6b"),routes:[{path:"/docs/1.x",component:u("/docs/1.x","d31"),routes:[{path:"/docs/1.x",component:u("/docs/1.x","f0e"),routes:[{path:"/docs/1.x/",component:u("/docs/1.x/","913"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/api-section/graphql",component:u("/docs/1.x/api-section/graphql","a98"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/api-section/openapi-and-swagger-ui",component:u("/docs/1.x/api-section/openapi-and-swagger-ui","ad6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/api-section/public-api-and-cors-requests",component:u("/docs/1.x/api-section/public-api-and-cors-requests","0e7"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/api-section/rest-blueprints",component:u("/docs/1.x/api-section/rest-blueprints","33b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/architecture/architecture-overview",component:u("/docs/1.x/architecture/architecture-overview","ecc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/architecture/controllers",component:u("/docs/1.x/architecture/controllers","6a9"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/architecture/hooks",component:u("/docs/1.x/architecture/hooks","6d1"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/architecture/initialization",component:u("/docs/1.x/architecture/initialization","b46"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/architecture/services-and-dependency-injection",component:u("/docs/1.x/architecture/services-and-dependency-injection","dd5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/administrators-and-roles",component:u("/docs/1.x/authentication-and-access-control/administrators-and-roles","9a1"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/groups-and-permissions",component:u("/docs/1.x/authentication-and-access-control/groups-and-permissions","355"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/jwt",component:u("/docs/1.x/authentication-and-access-control/jwt","4ce"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/password-management",component:u("/docs/1.x/authentication-and-access-control/password-management","1ea"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/quick-start",component:u("/docs/1.x/authentication-and-access-control/quick-start","3dc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/session-tokens",component:u("/docs/1.x/authentication-and-access-control/session-tokens","365"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/social-auth",component:u("/docs/1.x/authentication-and-access-control/social-auth","e57"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/authentication-and-access-control/user-class",component:u("/docs/1.x/authentication-and-access-control/user-class","985"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cloud/aws-beanstalk",component:u("/docs/1.x/cloud/aws-beanstalk","411"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cloud/firebase",component:u("/docs/1.x/cloud/firebase","db1"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/error-handling",component:u("/docs/1.x/cookbook/error-handling","779"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/expressjs",component:u("/docs/1.x/cookbook/expressjs","6af"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/generate-tokens",component:u("/docs/1.x/cookbook/generate-tokens","dae"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/limit-repeated-requests",component:u("/docs/1.x/cookbook/limit-repeated-requests","2eb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/not-found-page",component:u("/docs/1.x/cookbook/not-found-page","f60"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/request-body-size",component:u("/docs/1.x/cookbook/request-body-size","c18"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/root-imports",component:u("/docs/1.x/cookbook/root-imports","2b3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/cookbook/scheduling-jobs",component:u("/docs/1.x/cookbook/scheduling-jobs","265"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/databases/create-models-and-queries",component:u("/docs/1.x/databases/create-models-and-queries","f43"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/databases/generate-and-run-migrations",component:u("/docs/1.x/databases/generate-and-run-migrations","19b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/databases/mongodb",component:u("/docs/1.x/databases/mongodb","977"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/databases/typeorm",component:u("/docs/1.x/databases/typeorm","3d1"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/databases/using-another-orm",component:u("/docs/1.x/databases/using-another-orm","ae0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/deployment-and-environments/configuration",component:u("/docs/1.x/deployment-and-environments/configuration","7f8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/deployment-and-environments/ship-to-production",component:u("/docs/1.x/deployment-and-environments/ship-to-production","bc2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/development-environment/build-and-start-the-app",component:u("/docs/1.x/development-environment/build-and-start-the-app","07e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/development-environment/code-generation",component:u("/docs/1.x/development-environment/code-generation","e90"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/development-environment/create-and-run-scripts",component:u("/docs/1.x/development-environment/create-and-run-scripts","a58"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/development-environment/linting-and-code-style",component:u("/docs/1.x/development-environment/linting-and-code-style","e2a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/development-environment/vscode",component:u("/docs/1.x/development-environment/vscode","c1c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/file-system/local-and-cloud-storage",component:u("/docs/1.x/file-system/local-and-cloud-storage","de3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/file-system/upload-and-download-files",component:u("/docs/1.x/file-system/upload-and-download-files","655"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/frontend-integration/angular-react-vue",component:u("/docs/1.x/frontend-integration/angular-react-vue","1af"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/frontend-integration/jsx-server-side-rendering",component:u("/docs/1.x/frontend-integration/jsx-server-side-rendering","26c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/frontend-integration/nuxt.js",component:u("/docs/1.x/frontend-integration/nuxt.js","264"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/frontend-integration/single-page-applications",component:u("/docs/1.x/frontend-integration/single-page-applications","ba2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/security/csrf-protection",component:u("/docs/1.x/security/csrf-protection","5ae"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/security/http-headers-protection",component:u("/docs/1.x/security/http-headers-protection","93c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/security/xss-protection",component:u("/docs/1.x/security/xss-protection","c1a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/serializing-and-deserializing",component:u("/docs/1.x/serializing-and-deserializing","800"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/testing/e2e-testing",component:u("/docs/1.x/testing/e2e-testing","497"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/testing/introduction",component:u("/docs/1.x/testing/introduction","9fc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/testing/unit-testing",component:u("/docs/1.x/testing/unit-testing","d5b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-1-installation",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-1-installation","8e8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-2-introduction",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-2-introduction","626"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-3-the-todo-model",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-3-the-todo-model","253"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-4-the-shell-script-create-todo",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-4-the-shell-script-create-todo","5ba"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-5-the-rest-api",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-5-the-rest-api","914"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-6-validation-and-sanitization",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-6-validation-and-sanitization","bb8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/mongodb-todo-list/tuto-7-unit-testing",component:u("/docs/1.x/tutorials/mongodb-todo-list/tuto-7-unit-testing","bc4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-1-Introduction",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-1-Introduction","c4e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-2-the-user-and-todo-models",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-2-the-user-and-todo-models","7ac"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-3-the-shell-scripts",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-3-the-shell-scripts","69b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-5-auth-controllers-and-hooks",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-5-auth-controllers-and-hooks","196"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-6-todos-and-ownership",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-6-todos-and-ownership","8b2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-7-the-signup-page",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-7-the-signup-page","bbf"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/multi-user-todo-list/tuto-8-e2e-testing-and-authentication",component:u("/docs/1.x/tutorials/multi-user-todo-list/tuto-8-e2e-testing-and-authentication","49f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-1-installation",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-1-installation","474"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-2-introduction",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-2-introduction","c34"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-3-the-todo-model",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-3-the-todo-model","cfc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","feb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-5-the-rest-api",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-5-the-rest-api","772"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-6-validation-and-sanitization",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-6-validation-and-sanitization","ffd"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/tutorials/simple-todo-list/tuto-7-unit-testing",component:u("/docs/1.x/tutorials/simple-todo-list/tuto-7-unit-testing","1cf"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/utilities/logging-and-debugging",component:u("/docs/1.x/utilities/logging-and-debugging","0d9"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/utilities/templating",component:u("/docs/1.x/utilities/templating","e5e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/1.x/validation-and-sanitization",component:u("/docs/1.x/validation-and-sanitization","3b0"),exact:!0,sidebar:"someSidebar"}]}]},{path:"/docs/2.x",component:u("/docs/2.x","cdc"),routes:[{path:"/docs/2.x",component:u("/docs/2.x","22e"),routes:[{path:"/docs/2.x/",component:u("/docs/2.x/","e06"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/api-section/graphql",component:u("/docs/2.x/api-section/graphql","d3f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/api-section/gRPC",component:u("/docs/2.x/api-section/gRPC","9a0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/api-section/openapi-and-swagger-ui",component:u("/docs/2.x/api-section/openapi-and-swagger-ui","b23"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/api-section/public-api-and-cors-requests",component:u("/docs/2.x/api-section/public-api-and-cors-requests","d0c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/api-section/rest-blueprints",component:u("/docs/2.x/api-section/rest-blueprints","7d5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/architecture-overview",component:u("/docs/2.x/architecture/architecture-overview","24a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/configuration",component:u("/docs/2.x/architecture/configuration","3f5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/controllers",component:u("/docs/2.x/architecture/controllers","ef5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/error-handling",component:u("/docs/2.x/architecture/error-handling","077"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/hooks",component:u("/docs/2.x/architecture/hooks","9d4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/initialization",component:u("/docs/2.x/architecture/initialization","4e9"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/architecture/services-and-dependency-injection",component:u("/docs/2.x/architecture/services-and-dependency-injection","42c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/administrators-and-roles",component:u("/docs/2.x/authentication-and-access-control/administrators-and-roles","326"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/groups-and-permissions",component:u("/docs/2.x/authentication-and-access-control/groups-and-permissions","34e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/jwt",component:u("/docs/2.x/authentication-and-access-control/jwt","c17"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/password-management",component:u("/docs/2.x/authentication-and-access-control/password-management","af8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/quick-start",component:u("/docs/2.x/authentication-and-access-control/quick-start","b32"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/session-tokens",component:u("/docs/2.x/authentication-and-access-control/session-tokens","3c3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/social-auth",component:u("/docs/2.x/authentication-and-access-control/social-auth","a68"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/authentication-and-access-control/user-class",component:u("/docs/2.x/authentication-and-access-control/user-class","59d"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/common/conversions",component:u("/docs/2.x/common/conversions","211"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/common/generate-tokens",component:u("/docs/2.x/common/generate-tokens","4d6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/common/logging-and-debugging",component:u("/docs/2.x/common/logging-and-debugging","45f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/common/serializing-and-deserializing",component:u("/docs/2.x/common/serializing-and-deserializing","61f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/common/templating",component:u("/docs/2.x/common/templating","d4a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/common/validation-and-sanitization",component:u("/docs/2.x/common/validation-and-sanitization","3c3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/community/awesome-foal",component:u("/docs/2.x/community/awesome-foal","2de"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/comparison-with-other-frameworks/express-fastify",component:u("/docs/2.x/comparison-with-other-frameworks/express-fastify","498"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/cookbook/expressjs",component:u("/docs/2.x/cookbook/expressjs","5bc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/cookbook/limit-repeated-requests",component:u("/docs/2.x/cookbook/limit-repeated-requests","046"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/cookbook/not-found-page",component:u("/docs/2.x/cookbook/not-found-page","157"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/cookbook/request-body-size",component:u("/docs/2.x/cookbook/request-body-size","bae"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/cookbook/root-imports",component:u("/docs/2.x/cookbook/root-imports","89e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/cookbook/scheduling-jobs",component:u("/docs/2.x/cookbook/scheduling-jobs","b22"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/databases/create-models-and-queries",component:u("/docs/2.x/databases/create-models-and-queries","f1a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/databases/generate-and-run-migrations",component:u("/docs/2.x/databases/generate-and-run-migrations","d19"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/databases/mongodb",component:u("/docs/2.x/databases/mongodb","0cf"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/databases/typeorm",component:u("/docs/2.x/databases/typeorm","59f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/databases/using-another-orm",component:u("/docs/2.x/databases/using-another-orm","498"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/deployment-and-environments/checklist",component:u("/docs/2.x/deployment-and-environments/checklist","582"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/development-environment/build-and-start-the-app",component:u("/docs/2.x/development-environment/build-and-start-the-app","d1e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/development-environment/code-generation",component:u("/docs/2.x/development-environment/code-generation","46f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/development-environment/create-and-run-scripts",component:u("/docs/2.x/development-environment/create-and-run-scripts","b84"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/development-environment/linting-and-code-style",component:u("/docs/2.x/development-environment/linting-and-code-style","5cf"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/development-environment/vscode",component:u("/docs/2.x/development-environment/vscode","c20"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/file-system/local-and-cloud-storage",component:u("/docs/2.x/file-system/local-and-cloud-storage","4a3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/file-system/upload-and-download-files",component:u("/docs/2.x/file-system/upload-and-download-files","49a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/frontend-integration/angular-react-vue",component:u("/docs/2.x/frontend-integration/angular-react-vue","edb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/frontend-integration/jsx-server-side-rendering",component:u("/docs/2.x/frontend-integration/jsx-server-side-rendering","335"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/frontend-integration/nuxt.js",component:u("/docs/2.x/frontend-integration/nuxt.js","71c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/frontend-integration/single-page-applications",component:u("/docs/2.x/frontend-integration/single-page-applications","c40"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/security/csrf-protection",component:u("/docs/2.x/security/csrf-protection","8cd"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/security/http-headers-protection",component:u("/docs/2.x/security/http-headers-protection","d45"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/security/xss-protection",component:u("/docs/2.x/security/xss-protection","9c4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/testing/e2e-testing",component:u("/docs/2.x/testing/e2e-testing","2b4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/testing/introduction",component:u("/docs/2.x/testing/introduction","b05"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/testing/unit-testing",component:u("/docs/2.x/testing/unit-testing","056"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/1-introduction",component:u("/docs/2.x/tutorials/real-world-example-with-react/1-introduction","e71"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/10-auth-with-react",component:u("/docs/2.x/tutorials/real-world-example-with-react/10-auth-with-react","4dc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/11-sign-up",component:u("/docs/2.x/tutorials/real-world-example-with-react/11-sign-up","308"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/12-file-upload",component:u("/docs/2.x/tutorials/real-world-example-with-react/12-file-upload","232"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/13-csrf",component:u("/docs/2.x/tutorials/real-world-example-with-react/13-csrf","220"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/14-production-build",component:u("/docs/2.x/tutorials/real-world-example-with-react/14-production-build","c44"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/15-social-auth",component:u("/docs/2.x/tutorials/real-world-example-with-react/15-social-auth","d87"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/2-database-set-up",component:u("/docs/2.x/tutorials/real-world-example-with-react/2-database-set-up","ec0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/3-the-models",component:u("/docs/2.x/tutorials/real-world-example-with-react/3-the-models","ed7"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/4-the-shell-scripts",component:u("/docs/2.x/tutorials/real-world-example-with-react/4-the-shell-scripts","972"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/5-our-first-route",component:u("/docs/2.x/tutorials/real-world-example-with-react/5-our-first-route","637"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/6-swagger-interface",component:u("/docs/2.x/tutorials/real-world-example-with-react/6-swagger-interface","ace"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/7-add-frontend",component:u("/docs/2.x/tutorials/real-world-example-with-react/7-add-frontend","1b5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/8-authentication",component:u("/docs/2.x/tutorials/real-world-example-with-react/8-authentication","51b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/real-world-example-with-react/9-authenticated-api",component:u("/docs/2.x/tutorials/real-world-example-with-react/9-authenticated-api","1d0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/1-installation",component:u("/docs/2.x/tutorials/simple-todo-list/1-installation","5b0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/2-introduction",component:u("/docs/2.x/tutorials/simple-todo-list/2-introduction","954"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/3-the-todo-model",component:u("/docs/2.x/tutorials/simple-todo-list/3-the-todo-model","9b5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/4-the-shell-script-create-todo",component:u("/docs/2.x/tutorials/simple-todo-list/4-the-shell-script-create-todo","3a6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/5-the-rest-api",component:u("/docs/2.x/tutorials/simple-todo-list/5-the-rest-api","895"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/6-validation-and-sanitization",component:u("/docs/2.x/tutorials/simple-todo-list/6-validation-and-sanitization","5a3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/7-unit-testing",component:u("/docs/2.x/tutorials/simple-todo-list/7-unit-testing","749"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/tutorials/simple-todo-list/installation-troubleshooting",component:u("/docs/2.x/tutorials/simple-todo-list/installation-troubleshooting","f73"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/",component:u("/docs/2.x/upgrade-to-v2/","698"),exact:!0,sidebar:"someSidebar"},{path:"/docs/2.x/upgrade-to-v2/application-creation",component:u("/docs/2.x/upgrade-to-v2/application-creation","c05"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/cli-commands",component:u("/docs/2.x/upgrade-to-v2/cli-commands","b30"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/config-system",component:u("/docs/2.x/upgrade-to-v2/config-system","37d"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/custom-express-instance",component:u("/docs/2.x/upgrade-to-v2/custom-express-instance","2ed"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/error-handling",component:u("/docs/2.x/upgrade-to-v2/error-handling","ba9"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/file-upload-and-download",component:u("/docs/2.x/upgrade-to-v2/file-upload-and-download","8cf"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/jwt-and-csrf",component:u("/docs/2.x/upgrade-to-v2/jwt-and-csrf","cda"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/mongodb",component:u("/docs/2.x/upgrade-to-v2/mongodb","d29"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/openapi",component:u("/docs/2.x/upgrade-to-v2/openapi","012"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/service-and-app-initialization",component:u("/docs/2.x/upgrade-to-v2/service-and-app-initialization","df3"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/session-tokens",component:u("/docs/2.x/upgrade-to-v2/session-tokens","a26"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/template-engine",component:u("/docs/2.x/upgrade-to-v2/template-engine","201"),exact:!0},{path:"/docs/2.x/upgrade-to-v2/validation-hooks",component:u("/docs/2.x/upgrade-to-v2/validation-hooks","5e6"),exact:!0},{path:"/docs/2.x/websockets",component:u("/docs/2.x/websockets","661"),exact:!0,sidebar:"someSidebar"}]}]},{path:"/docs/3.x",component:u("/docs/3.x","aac"),routes:[{path:"/docs/3.x",component:u("/docs/3.x","9d1"),routes:[{path:"/docs/3.x/",component:u("/docs/3.x/","484"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/architecture-overview",component:u("/docs/3.x/architecture/architecture-overview","009"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/configuration",component:u("/docs/3.x/architecture/configuration","77e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/controllers",component:u("/docs/3.x/architecture/controllers","9bd"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/error-handling",component:u("/docs/3.x/architecture/error-handling","0c9"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/hooks",component:u("/docs/3.x/architecture/hooks","93e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/initialization",component:u("/docs/3.x/architecture/initialization","048"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/architecture/services-and-dependency-injection",component:u("/docs/3.x/architecture/services-and-dependency-injection","727"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authentication/jwt",component:u("/docs/3.x/authentication/jwt","5e3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authentication/password-management",component:u("/docs/3.x/authentication/password-management","a4c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authentication/quick-start",component:u("/docs/3.x/authentication/quick-start","b87"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authentication/session-tokens",component:u("/docs/3.x/authentication/session-tokens","369"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authentication/social-auth",component:u("/docs/3.x/authentication/social-auth","4b1"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authentication/user-class",component:u("/docs/3.x/authentication/user-class","02f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authorization/administrators-and-roles",component:u("/docs/3.x/authorization/administrators-and-roles","70e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/authorization/groups-and-permissions",component:u("/docs/3.x/authorization/groups-and-permissions","cad"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/cli/code-generation",component:u("/docs/3.x/cli/code-generation","72f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/cli/commands",component:u("/docs/3.x/cli/commands","43a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/cli/linting-and-code-style",component:u("/docs/3.x/cli/linting-and-code-style","4bc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/cli/shell-scripts",component:u("/docs/3.x/cli/shell-scripts","516"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/expressjs",component:u("/docs/3.x/common/expressjs","e3e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/file-storage/local-and-cloud-storage",component:u("/docs/3.x/common/file-storage/local-and-cloud-storage","1be"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/file-storage/upload-and-download-files",component:u("/docs/3.x/common/file-storage/upload-and-download-files","ab0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/graphql",component:u("/docs/3.x/common/graphql","701"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/gRPC",component:u("/docs/3.x/common/gRPC","7fd"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/logging",component:u("/docs/3.x/common/logging","e68"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/openapi-and-swagger-ui",component:u("/docs/3.x/common/openapi-and-swagger-ui","967"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/rest-blueprints",component:u("/docs/3.x/common/rest-blueprints","d07"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/serialization",component:u("/docs/3.x/common/serialization","175"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/task-scheduling",component:u("/docs/3.x/common/task-scheduling","92c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/utilities",component:u("/docs/3.x/common/utilities","15f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/validation-and-sanitization",component:u("/docs/3.x/common/validation-and-sanitization","203"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/common/websockets",component:u("/docs/3.x/common/websockets","748"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/community/awesome-foal",component:u("/docs/3.x/community/awesome-foal","f0f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/comparison-with-other-frameworks/express-fastify",component:u("/docs/3.x/comparison-with-other-frameworks/express-fastify","312"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/databases/other-orm/introduction",component:u("/docs/3.x/databases/other-orm/introduction","e00"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/databases/other-orm/prisma",component:u("/docs/3.x/databases/other-orm/prisma","615"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/databases/typeorm/create-models-and-queries",component:u("/docs/3.x/databases/typeorm/create-models-and-queries","60e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/databases/typeorm/generate-and-run-migrations",component:u("/docs/3.x/databases/typeorm/generate-and-run-migrations","82e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/databases/typeorm/introduction",component:u("/docs/3.x/databases/typeorm/introduction","c20"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/databases/typeorm/mongodb",component:u("/docs/3.x/databases/typeorm/mongodb","4e2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/deployment-and-environments/checklist",component:u("/docs/3.x/deployment-and-environments/checklist","fae"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/frontend/angular-react-vue",component:u("/docs/3.x/frontend/angular-react-vue","bc8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/frontend/not-found-page",component:u("/docs/3.x/frontend/not-found-page","285"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/frontend/nuxt.js",component:u("/docs/3.x/frontend/nuxt.js","d2b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/frontend/server-side-rendering",component:u("/docs/3.x/frontend/server-side-rendering","c85"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/frontend/single-page-applications",component:u("/docs/3.x/frontend/single-page-applications","7dc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/security/body-size-limiting",component:u("/docs/3.x/security/body-size-limiting","0ba"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/security/cors",component:u("/docs/3.x/security/cors","8a5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/security/csrf-protection",component:u("/docs/3.x/security/csrf-protection","8f3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/security/http-headers-protection",component:u("/docs/3.x/security/http-headers-protection","7a8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/security/rate-limiting",component:u("/docs/3.x/security/rate-limiting","229"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/testing/e2e-testing",component:u("/docs/3.x/testing/e2e-testing","b5c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/testing/introduction",component:u("/docs/3.x/testing/introduction","9f7"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/testing/unit-testing",component:u("/docs/3.x/testing/unit-testing","4bc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/1-introduction",component:u("/docs/3.x/tutorials/real-world-example-with-react/1-introduction","74b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/10-auth-with-react",component:u("/docs/3.x/tutorials/real-world-example-with-react/10-auth-with-react","f59"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/11-sign-up",component:u("/docs/3.x/tutorials/real-world-example-with-react/11-sign-up","5ce"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/12-file-upload",component:u("/docs/3.x/tutorials/real-world-example-with-react/12-file-upload","6eb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/13-csrf",component:u("/docs/3.x/tutorials/real-world-example-with-react/13-csrf","90b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/14-production-build",component:u("/docs/3.x/tutorials/real-world-example-with-react/14-production-build","7a3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/15-social-auth",component:u("/docs/3.x/tutorials/real-world-example-with-react/15-social-auth","1ea"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/2-database-set-up",component:u("/docs/3.x/tutorials/real-world-example-with-react/2-database-set-up","8bb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/3-the-models",component:u("/docs/3.x/tutorials/real-world-example-with-react/3-the-models","ceb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/4-the-shell-scripts",component:u("/docs/3.x/tutorials/real-world-example-with-react/4-the-shell-scripts","af6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/5-our-first-route",component:u("/docs/3.x/tutorials/real-world-example-with-react/5-our-first-route","f26"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/6-swagger-interface",component:u("/docs/3.x/tutorials/real-world-example-with-react/6-swagger-interface","9a6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/7-add-frontend",component:u("/docs/3.x/tutorials/real-world-example-with-react/7-add-frontend","9c4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/8-authentication",component:u("/docs/3.x/tutorials/real-world-example-with-react/8-authentication","6e6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/real-world-example-with-react/9-authenticated-api",component:u("/docs/3.x/tutorials/real-world-example-with-react/9-authenticated-api","198"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/1-installation",component:u("/docs/3.x/tutorials/simple-todo-list/1-installation","107"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/2-introduction",component:u("/docs/3.x/tutorials/simple-todo-list/2-introduction","a69"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/3-the-todo-model",component:u("/docs/3.x/tutorials/simple-todo-list/3-the-todo-model","807"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/4-the-shell-script-create-todo",component:u("/docs/3.x/tutorials/simple-todo-list/4-the-shell-script-create-todo","0f2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/5-the-rest-api",component:u("/docs/3.x/tutorials/simple-todo-list/5-the-rest-api","683"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/6-validation-and-sanitization",component:u("/docs/3.x/tutorials/simple-todo-list/6-validation-and-sanitization","c75"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/7-unit-testing",component:u("/docs/3.x/tutorials/simple-todo-list/7-unit-testing","88f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/3.x/tutorials/simple-todo-list/installation-troubleshooting",component:u("/docs/3.x/tutorials/simple-todo-list/installation-troubleshooting","30c"),exact:!0},{path:"/docs/3.x/upgrade-to-v3/",component:u("/docs/3.x/upgrade-to-v3/","0c5"),exact:!0,sidebar:"someSidebar"}]}]},{path:"/docs",component:u("/docs","d9b"),routes:[{path:"/docs",component:u("/docs","cda"),routes:[{path:"/docs/",component:u("/docs/","271"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/architecture-overview",component:u("/docs/architecture/architecture-overview","6d2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/configuration",component:u("/docs/architecture/configuration","17f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/controllers",component:u("/docs/architecture/controllers","a34"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/error-handling",component:u("/docs/architecture/error-handling","5c4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/hooks",component:u("/docs/architecture/hooks","4b7"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/initialization",component:u("/docs/architecture/initialization","ad4"),exact:!0,sidebar:"someSidebar"},{path:"/docs/architecture/services-and-dependency-injection",component:u("/docs/architecture/services-and-dependency-injection","106"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authentication/jwt",component:u("/docs/authentication/jwt","172"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authentication/password-management",component:u("/docs/authentication/password-management","476"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authentication/quick-start",component:u("/docs/authentication/quick-start","da9"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authentication/session-tokens",component:u("/docs/authentication/session-tokens","aab"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authentication/social-auth",component:u("/docs/authentication/social-auth","ff2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authentication/user-class",component:u("/docs/authentication/user-class","cfe"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authorization/administrators-and-roles",component:u("/docs/authorization/administrators-and-roles","c3c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/authorization/groups-and-permissions",component:u("/docs/authorization/groups-and-permissions","bda"),exact:!0,sidebar:"someSidebar"},{path:"/docs/cli/code-generation",component:u("/docs/cli/code-generation","18a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/cli/commands",component:u("/docs/cli/commands","f6b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/cli/linting-and-code-style",component:u("/docs/cli/linting-and-code-style","371"),exact:!0,sidebar:"someSidebar"},{path:"/docs/cli/shell-scripts",component:u("/docs/cli/shell-scripts","8f3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/async-tasks",component:u("/docs/common/async-tasks","ae8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/expressjs",component:u("/docs/common/expressjs","5c2"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/file-storage/local-and-cloud-storage",component:u("/docs/common/file-storage/local-and-cloud-storage","9a0"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/file-storage/upload-and-download-files",component:u("/docs/common/file-storage/upload-and-download-files","8b9"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/graphql",component:u("/docs/common/graphql","a1b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/gRPC",component:u("/docs/common/gRPC","b23"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/logging",component:u("/docs/common/logging","0cd"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/openapi-and-swagger-ui",component:u("/docs/common/openapi-and-swagger-ui","e7c"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/rest-blueprints",component:u("/docs/common/rest-blueprints","a3d"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/serialization",component:u("/docs/common/serialization","361"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/utilities",component:u("/docs/common/utilities","26e"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/validation-and-sanitization",component:u("/docs/common/validation-and-sanitization","ed3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/common/websockets",component:u("/docs/common/websockets","2f3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/community/awesome-foal",component:u("/docs/community/awesome-foal","dcf"),exact:!0,sidebar:"someSidebar"},{path:"/docs/comparison-with-other-frameworks/express-fastify",component:u("/docs/comparison-with-other-frameworks/express-fastify","a86"),exact:!0,sidebar:"someSidebar"},{path:"/docs/databases/other-orm/introduction",component:u("/docs/databases/other-orm/introduction","59a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/databases/other-orm/prisma",component:u("/docs/databases/other-orm/prisma","d99"),exact:!0,sidebar:"someSidebar"},{path:"/docs/databases/typeorm/create-models-and-queries",component:u("/docs/databases/typeorm/create-models-and-queries","b86"),exact:!0,sidebar:"someSidebar"},{path:"/docs/databases/typeorm/generate-and-run-migrations",component:u("/docs/databases/typeorm/generate-and-run-migrations","b97"),exact:!0,sidebar:"someSidebar"},{path:"/docs/databases/typeorm/introduction",component:u("/docs/databases/typeorm/introduction","899"),exact:!0,sidebar:"someSidebar"},{path:"/docs/databases/typeorm/mongodb",component:u("/docs/databases/typeorm/mongodb","d44"),exact:!0,sidebar:"someSidebar"},{path:"/docs/deployment-and-environments/checklist",component:u("/docs/deployment-and-environments/checklist","ad5"),exact:!0,sidebar:"someSidebar"},{path:"/docs/frontend/angular-react-vue",component:u("/docs/frontend/angular-react-vue","04a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/frontend/not-found-page",component:u("/docs/frontend/not-found-page","cfa"),exact:!0,sidebar:"someSidebar"},{path:"/docs/frontend/nuxt.js",component:u("/docs/frontend/nuxt.js","6cc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/frontend/server-side-rendering",component:u("/docs/frontend/server-side-rendering","015"),exact:!0,sidebar:"someSidebar"},{path:"/docs/frontend/single-page-applications",component:u("/docs/frontend/single-page-applications","c38"),exact:!0,sidebar:"someSidebar"},{path:"/docs/security/body-size-limiting",component:u("/docs/security/body-size-limiting","c1a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/security/cors",component:u("/docs/security/cors","0f6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/security/csrf-protection",component:u("/docs/security/csrf-protection","0b3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/security/http-headers-protection",component:u("/docs/security/http-headers-protection","06a"),exact:!0,sidebar:"someSidebar"},{path:"/docs/security/rate-limiting",component:u("/docs/security/rate-limiting","6ec"),exact:!0,sidebar:"someSidebar"},{path:"/docs/testing/e2e-testing",component:u("/docs/testing/e2e-testing","431"),exact:!0,sidebar:"someSidebar"},{path:"/docs/testing/introduction",component:u("/docs/testing/introduction","02d"),exact:!0,sidebar:"someSidebar"},{path:"/docs/testing/unit-testing",component:u("/docs/testing/unit-testing","8e6"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/1-introduction",component:u("/docs/tutorials/real-world-example-with-react/1-introduction","42b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/10-auth-with-react",component:u("/docs/tutorials/real-world-example-with-react/10-auth-with-react","d8d"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/11-sign-up",component:u("/docs/tutorials/real-world-example-with-react/11-sign-up","f13"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/12-file-upload",component:u("/docs/tutorials/real-world-example-with-react/12-file-upload","30d"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/13-csrf",component:u("/docs/tutorials/real-world-example-with-react/13-csrf","328"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/14-production-build",component:u("/docs/tutorials/real-world-example-with-react/14-production-build","908"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/15-social-auth",component:u("/docs/tutorials/real-world-example-with-react/15-social-auth","c35"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/2-database-set-up",component:u("/docs/tutorials/real-world-example-with-react/2-database-set-up","793"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/3-the-models",component:u("/docs/tutorials/real-world-example-with-react/3-the-models","842"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts",component:u("/docs/tutorials/real-world-example-with-react/4-the-shell-scripts","5ec"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/5-our-first-route",component:u("/docs/tutorials/real-world-example-with-react/5-our-first-route","3ef"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/6-swagger-interface",component:u("/docs/tutorials/real-world-example-with-react/6-swagger-interface","1d3"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/7-add-frontend",component:u("/docs/tutorials/real-world-example-with-react/7-add-frontend","faa"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/8-authentication",component:u("/docs/tutorials/real-world-example-with-react/8-authentication","011"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/real-world-example-with-react/9-authenticated-api",component:u("/docs/tutorials/real-world-example-with-react/9-authenticated-api","86b"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/1-installation",component:u("/docs/tutorials/simple-todo-list/1-installation","eb8"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/2-introduction",component:u("/docs/tutorials/simple-todo-list/2-introduction","edb"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/3-the-todo-model",component:u("/docs/tutorials/simple-todo-list/3-the-todo-model","1ae"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo",component:u("/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo","508"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/5-the-rest-api",component:u("/docs/tutorials/simple-todo-list/5-the-rest-api","bdc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/6-validation-and-sanitization",component:u("/docs/tutorials/simple-todo-list/6-validation-and-sanitization","16f"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/7-unit-testing",component:u("/docs/tutorials/simple-todo-list/7-unit-testing","4bc"),exact:!0,sidebar:"someSidebar"},{path:"/docs/tutorials/simple-todo-list/installation-troubleshooting",component:u("/docs/tutorials/simple-todo-list/installation-troubleshooting","ab0"),exact:!0}]}]}]},{path:"/",component:u("/","71c"),exact:!0},{path:"*",component:u("*")}]},6125:(e,t,n)=>{"use strict";n.d(t,{o:()=>a,x:()=>i});var o=n(96540),r=n(74848);const a=o.createContext(!1);function i(e){let{children:t}=e;const[n,i]=(0,o.useState)(!1);return(0,o.useEffect)((()=>{i(!0)}),[]),(0,r.jsx)(a.Provider,{value:n,children:t})}},38536:(e,t,n)=>{"use strict";var o=n(96540),r=n(5338),a=n(54625),i=n(80545),s=n(38193);const l=[n(37651),n(10119),n(26134),n(76294),n(89888)];var c=n(35947),d=n(56347),u=n(22831),p=n(74848);function m(e){let{children:t}=e;return(0,p.jsx)(p.Fragment,{children:t})}var f=n(5260),h=n(44586),b=n(86025),g=n(6342),v=n(69024),x=n(32131),y=n(14090),w=n(2967),k=n(70440),S=n(41463);function _(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,h.A)(),o=(0,x.o)(),r=n[e].htmlLang,a=e=>e.replace("-","_");return(0,p.jsxs)(f.A,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,p.jsx)("link",{rel:"alternate",href:o.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,p.jsx)("link",{rel:"alternate",href:o.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,p.jsx)("meta",{property:"og:locale",content:a(r)}),Object.values(n).filter((e=>r!==e.htmlLang)).map((e=>(0,p.jsx)("meta",{property:"og:locale:alternate",content:a(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function E(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,h.A)(),o=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,h.A)(),{pathname:o}=(0,d.zy)();return e+(0,k.applyTrailingSlash)((0,b.A)(o),{trailingSlash:n,baseUrl:t})}(),r=t?`${n}${t}`:o;return(0,p.jsxs)(f.A,{children:[(0,p.jsx)("meta",{property:"og:url",content:r}),(0,p.jsx)("link",{rel:"canonical",href:r})]})}function C(){const{i18n:{currentLocale:e}}=(0,h.A)(),{metadata:t,image:n}=(0,g.p)();return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(f.A,{children:[(0,p.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,p.jsx)("body",{className:y.w})]}),n&&(0,p.jsx)(v.be,{image:n}),(0,p.jsx)(E,{}),(0,p.jsx)(_,{}),(0,p.jsx)(S.A,{tag:w.Cy,locale:e}),(0,p.jsx)(f.A,{children:t.map(((e,t)=>(0,p.jsx)("meta",{...e},t)))})]})}const T=new Map;function A(e){if(T.has(e.pathname))return{...e,pathname:T.get(e.pathname)};if((0,u.u)(c.A,e.pathname).some((e=>{let{route:t}=e;return!0===t.exact})))return T.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return T.set(e.pathname,t),{...e,pathname:t}}var j=n(6125),R=n(26988),P=n(205);function L(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),o=1;o{const o=t.default?.[e]??t[e];return o?.(...n)}));return()=>r.forEach((e=>e?.()))}const N=function(e){let{children:t,location:n,previousLocation:o}=e;return(0,P.A)((()=>{o!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const o=t.pathname===n.pathname,r=t.hash===n.hash,a=t.search===n.search;if(o&&r&&!a)return;const{hash:i}=t;if(i){const e=decodeURIComponent(i.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:o}),L("onRouteDidUpdate",{previousLocation:o,location:n}))}),[o,n]),t};function O(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,u.u)(c.A,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class D extends o.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=s.A.canUseDOM?L("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=L("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),O(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,p.jsx)(N,{previousLocation:this.previousLocation,location:t,children:(0,p.jsx)(d.qh,{location:t,render:()=>e})})}}const z=D,M="__docusaurus-base-url-issue-banner-container",I="__docusaurus-base-url-issue-banner",F="__docusaurus-base-url-issue-banner-suggestion-container";function B(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${M}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/{if("undefined"==typeof document)return void n();const o=document.createElement("link");o.setAttribute("rel","prefetch"),o.setAttribute("href",e),o.onload=()=>t(),o.onerror=()=>n();const r=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;r?.appendChild(o)}))}:function(e){return new Promise(((t,n)=>{const o=new XMLHttpRequest;o.open("GET",e,!0),o.withCredentials=!0,o.onload=()=>{200===o.status?t():n()},o.send(null)}))};var Y=n(86921);const Z=new Set,X=new Set,J=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee={prefetch(e){if(!(e=>!J()&&!X.has(e)&&!Z.has(e))(e))return!1;Z.add(e);const t=(0,u.u)(c.A,e).flatMap((e=>{return t=e.route.path,Object.entries(K).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,Y.A)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?Q(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!J()&&!X.has(e))(e)&&(X.add(e),O(e))},te=Object.freeze(ee),ne=Boolean(!0);if(s.A.canUseDOM){window.docusaurus=te;const e=document.getElementById("__docusaurus"),t=(0,p.jsx)(i.vd,{children:(0,p.jsx)(a.Kd,{children:(0,p.jsx)(W,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},s=()=>{if(ne)o.startTransition((()=>{r.hydrateRoot(e,t,{onRecoverableError:n})}));else{const a=r.createRoot(e,{onRecoverableError:n});o.startTransition((()=>{a.render(t)}))}};O(window.location.pathname).then(s)}},26988:(e,t,n)=>{"use strict";n.d(t,{o:()=>u,l:()=>p});var o=n(96540),r=n(4784);const a=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"v4","isLast":true,"path":"/docs","mainDocId":"README","docs":[{"id":"architecture/architecture-overview","path":"/docs/architecture/architecture-overview","sidebar":"someSidebar"},{"id":"architecture/configuration","path":"/docs/architecture/configuration","sidebar":"someSidebar"},{"id":"architecture/controllers","path":"/docs/architecture/controllers","sidebar":"someSidebar"},{"id":"architecture/error-handling","path":"/docs/architecture/error-handling","sidebar":"someSidebar"},{"id":"architecture/hooks","path":"/docs/architecture/hooks","sidebar":"someSidebar"},{"id":"architecture/initialization","path":"/docs/architecture/initialization","sidebar":"someSidebar"},{"id":"architecture/services-and-dependency-injection","path":"/docs/architecture/services-and-dependency-injection","sidebar":"someSidebar"},{"id":"authentication/jwt","path":"/docs/authentication/jwt","sidebar":"someSidebar"},{"id":"authentication/password-management","path":"/docs/authentication/password-management","sidebar":"someSidebar"},{"id":"authentication/quick-start","path":"/docs/authentication/quick-start","sidebar":"someSidebar"},{"id":"authentication/session-tokens","path":"/docs/authentication/session-tokens","sidebar":"someSidebar"},{"id":"authentication/social-auth","path":"/docs/authentication/social-auth","sidebar":"someSidebar"},{"id":"authentication/user-class","path":"/docs/authentication/user-class","sidebar":"someSidebar"},{"id":"authorization/administrators-and-roles","path":"/docs/authorization/administrators-and-roles","sidebar":"someSidebar"},{"id":"authorization/groups-and-permissions","path":"/docs/authorization/groups-and-permissions","sidebar":"someSidebar"},{"id":"cli/code-generation","path":"/docs/cli/code-generation","sidebar":"someSidebar"},{"id":"cli/commands","path":"/docs/cli/commands","sidebar":"someSidebar"},{"id":"cli/linting-and-code-style","path":"/docs/cli/linting-and-code-style","sidebar":"someSidebar"},{"id":"cli/shell-scripts","path":"/docs/cli/shell-scripts","sidebar":"someSidebar"},{"id":"common/async-tasks","path":"/docs/common/async-tasks","sidebar":"someSidebar"},{"id":"common/expressjs","path":"/docs/common/expressjs","sidebar":"someSidebar"},{"id":"common/file-storage/local-and-cloud-storage","path":"/docs/common/file-storage/local-and-cloud-storage","sidebar":"someSidebar"},{"id":"common/file-storage/upload-and-download-files","path":"/docs/common/file-storage/upload-and-download-files","sidebar":"someSidebar"},{"id":"common/graphql","path":"/docs/common/graphql","sidebar":"someSidebar"},{"id":"common/gRPC","path":"/docs/common/gRPC","sidebar":"someSidebar"},{"id":"common/logging","path":"/docs/common/logging","sidebar":"someSidebar"},{"id":"common/openapi-and-swagger-ui","path":"/docs/common/openapi-and-swagger-ui","sidebar":"someSidebar"},{"id":"common/rest-blueprints","path":"/docs/common/rest-blueprints","sidebar":"someSidebar"},{"id":"common/serialization","path":"/docs/common/serialization","sidebar":"someSidebar"},{"id":"common/utilities","path":"/docs/common/utilities","sidebar":"someSidebar"},{"id":"common/validation-and-sanitization","path":"/docs/common/validation-and-sanitization","sidebar":"someSidebar"},{"id":"common/websockets","path":"/docs/common/websockets","sidebar":"someSidebar"},{"id":"community/awesome-foal","path":"/docs/community/awesome-foal","sidebar":"someSidebar"},{"id":"comparison-with-other-frameworks/express-fastify","path":"/docs/comparison-with-other-frameworks/express-fastify","sidebar":"someSidebar"},{"id":"databases/other-orm/introduction","path":"/docs/databases/other-orm/introduction","sidebar":"someSidebar"},{"id":"databases/other-orm/prisma","path":"/docs/databases/other-orm/prisma","sidebar":"someSidebar"},{"id":"databases/typeorm/create-models-and-queries","path":"/docs/databases/typeorm/create-models-and-queries","sidebar":"someSidebar"},{"id":"databases/typeorm/generate-and-run-migrations","path":"/docs/databases/typeorm/generate-and-run-migrations","sidebar":"someSidebar"},{"id":"databases/typeorm/introduction","path":"/docs/databases/typeorm/introduction","sidebar":"someSidebar"},{"id":"databases/typeorm/mongodb","path":"/docs/databases/typeorm/mongodb","sidebar":"someSidebar"},{"id":"deployment-and-environments/checklist","path":"/docs/deployment-and-environments/checklist","sidebar":"someSidebar"},{"id":"frontend/angular-react-vue","path":"/docs/frontend/angular-react-vue","sidebar":"someSidebar"},{"id":"frontend/not-found-page","path":"/docs/frontend/not-found-page","sidebar":"someSidebar"},{"id":"frontend/nuxt.js","path":"/docs/frontend/nuxt.js","sidebar":"someSidebar"},{"id":"frontend/server-side-rendering","path":"/docs/frontend/server-side-rendering","sidebar":"someSidebar"},{"id":"frontend/single-page-applications","path":"/docs/frontend/single-page-applications","sidebar":"someSidebar"},{"id":"README","path":"/docs/","sidebar":"someSidebar"},{"id":"security/body-size-limiting","path":"/docs/security/body-size-limiting","sidebar":"someSidebar"},{"id":"security/cors","path":"/docs/security/cors","sidebar":"someSidebar"},{"id":"security/csrf-protection","path":"/docs/security/csrf-protection","sidebar":"someSidebar"},{"id":"security/http-headers-protection","path":"/docs/security/http-headers-protection","sidebar":"someSidebar"},{"id":"security/rate-limiting","path":"/docs/security/rate-limiting","sidebar":"someSidebar"},{"id":"testing/e2e-testing","path":"/docs/testing/e2e-testing","sidebar":"someSidebar"},{"id":"testing/introduction","path":"/docs/testing/introduction","sidebar":"someSidebar"},{"id":"testing/unit-testing","path":"/docs/testing/unit-testing","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-1-introduction","path":"/docs/tutorials/real-world-example-with-react/1-introduction","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","path":"/docs/tutorials/real-world-example-with-react/10-auth-with-react","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-11-sign-up","path":"/docs/tutorials/real-world-example-with-react/11-sign-up","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-12-file-upload","path":"/docs/tutorials/real-world-example-with-react/12-file-upload","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-13-csrf","path":"/docs/tutorials/real-world-example-with-react/13-csrf","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-14-production-build","path":"/docs/tutorials/real-world-example-with-react/14-production-build","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-15-social-auth","path":"/docs/tutorials/real-world-example-with-react/15-social-auth","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-2-database-set-up","path":"/docs/tutorials/real-world-example-with-react/2-database-set-up","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-3-the-models","path":"/docs/tutorials/real-world-example-with-react/3-the-models","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","path":"/docs/tutorials/real-world-example-with-react/4-the-shell-scripts","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-5-our-first-route","path":"/docs/tutorials/real-world-example-with-react/5-our-first-route","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","path":"/docs/tutorials/real-world-example-with-react/6-swagger-interface","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-7-add-frontend","path":"/docs/tutorials/real-world-example-with-react/7-add-frontend","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-8-authentication","path":"/docs/tutorials/real-world-example-with-react/8-authentication","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","path":"/docs/tutorials/real-world-example-with-react/9-authenticated-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/installation-troubleshooting","path":"/docs/tutorials/simple-todo-list/installation-troubleshooting"},{"id":"tutorials/simple-todo-list/tuto-1-installation","path":"/docs/tutorials/simple-todo-list/1-installation","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-2-introduction","path":"/docs/tutorials/simple-todo-list/2-introduction","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-3-the-todo-model","path":"/docs/tutorials/simple-todo-list/3-the-todo-model","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","path":"/docs/tutorials/simple-todo-list/4-the-shell-script-create-todo","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-5-the-rest-api","path":"/docs/tutorials/simple-todo-list/5-the-rest-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","path":"/docs/tutorials/simple-todo-list/6-validation-and-sanitization","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-7-unit-testing","path":"/docs/tutorials/simple-todo-list/7-unit-testing","sidebar":"someSidebar"}],"draftIds":[],"sidebars":{"someSidebar":{"link":{"path":"/docs/","label":"README"}}}},{"name":"3.x","label":"v3","isLast":false,"path":"/docs/3.x","mainDocId":"README","docs":[{"id":"architecture/architecture-overview","path":"/docs/3.x/architecture/architecture-overview","sidebar":"someSidebar"},{"id":"architecture/configuration","path":"/docs/3.x/architecture/configuration","sidebar":"someSidebar"},{"id":"architecture/controllers","path":"/docs/3.x/architecture/controllers","sidebar":"someSidebar"},{"id":"architecture/error-handling","path":"/docs/3.x/architecture/error-handling","sidebar":"someSidebar"},{"id":"architecture/hooks","path":"/docs/3.x/architecture/hooks","sidebar":"someSidebar"},{"id":"architecture/initialization","path":"/docs/3.x/architecture/initialization","sidebar":"someSidebar"},{"id":"architecture/services-and-dependency-injection","path":"/docs/3.x/architecture/services-and-dependency-injection","sidebar":"someSidebar"},{"id":"authentication/jwt","path":"/docs/3.x/authentication/jwt","sidebar":"someSidebar"},{"id":"authentication/password-management","path":"/docs/3.x/authentication/password-management","sidebar":"someSidebar"},{"id":"authentication/quick-start","path":"/docs/3.x/authentication/quick-start","sidebar":"someSidebar"},{"id":"authentication/session-tokens","path":"/docs/3.x/authentication/session-tokens","sidebar":"someSidebar"},{"id":"authentication/social-auth","path":"/docs/3.x/authentication/social-auth","sidebar":"someSidebar"},{"id":"authentication/user-class","path":"/docs/3.x/authentication/user-class","sidebar":"someSidebar"},{"id":"authorization/administrators-and-roles","path":"/docs/3.x/authorization/administrators-and-roles","sidebar":"someSidebar"},{"id":"authorization/groups-and-permissions","path":"/docs/3.x/authorization/groups-and-permissions","sidebar":"someSidebar"},{"id":"cli/code-generation","path":"/docs/3.x/cli/code-generation","sidebar":"someSidebar"},{"id":"cli/commands","path":"/docs/3.x/cli/commands","sidebar":"someSidebar"},{"id":"cli/linting-and-code-style","path":"/docs/3.x/cli/linting-and-code-style","sidebar":"someSidebar"},{"id":"cli/shell-scripts","path":"/docs/3.x/cli/shell-scripts","sidebar":"someSidebar"},{"id":"common/expressjs","path":"/docs/3.x/common/expressjs","sidebar":"someSidebar"},{"id":"common/file-storage/local-and-cloud-storage","path":"/docs/3.x/common/file-storage/local-and-cloud-storage","sidebar":"someSidebar"},{"id":"common/file-storage/upload-and-download-files","path":"/docs/3.x/common/file-storage/upload-and-download-files","sidebar":"someSidebar"},{"id":"common/graphql","path":"/docs/3.x/common/graphql","sidebar":"someSidebar"},{"id":"common/gRPC","path":"/docs/3.x/common/gRPC","sidebar":"someSidebar"},{"id":"common/logging","path":"/docs/3.x/common/logging","sidebar":"someSidebar"},{"id":"common/openapi-and-swagger-ui","path":"/docs/3.x/common/openapi-and-swagger-ui","sidebar":"someSidebar"},{"id":"common/rest-blueprints","path":"/docs/3.x/common/rest-blueprints","sidebar":"someSidebar"},{"id":"common/serialization","path":"/docs/3.x/common/serialization","sidebar":"someSidebar"},{"id":"common/task-scheduling","path":"/docs/3.x/common/task-scheduling","sidebar":"someSidebar"},{"id":"common/utilities","path":"/docs/3.x/common/utilities","sidebar":"someSidebar"},{"id":"common/validation-and-sanitization","path":"/docs/3.x/common/validation-and-sanitization","sidebar":"someSidebar"},{"id":"common/websockets","path":"/docs/3.x/common/websockets","sidebar":"someSidebar"},{"id":"community/awesome-foal","path":"/docs/3.x/community/awesome-foal","sidebar":"someSidebar"},{"id":"comparison-with-other-frameworks/express-fastify","path":"/docs/3.x/comparison-with-other-frameworks/express-fastify","sidebar":"someSidebar"},{"id":"databases/other-orm/introduction","path":"/docs/3.x/databases/other-orm/introduction","sidebar":"someSidebar"},{"id":"databases/other-orm/prisma","path":"/docs/3.x/databases/other-orm/prisma","sidebar":"someSidebar"},{"id":"databases/typeorm/create-models-and-queries","path":"/docs/3.x/databases/typeorm/create-models-and-queries","sidebar":"someSidebar"},{"id":"databases/typeorm/generate-and-run-migrations","path":"/docs/3.x/databases/typeorm/generate-and-run-migrations","sidebar":"someSidebar"},{"id":"databases/typeorm/introduction","path":"/docs/3.x/databases/typeorm/introduction","sidebar":"someSidebar"},{"id":"databases/typeorm/mongodb","path":"/docs/3.x/databases/typeorm/mongodb","sidebar":"someSidebar"},{"id":"deployment-and-environments/checklist","path":"/docs/3.x/deployment-and-environments/checklist","sidebar":"someSidebar"},{"id":"frontend/angular-react-vue","path":"/docs/3.x/frontend/angular-react-vue","sidebar":"someSidebar"},{"id":"frontend/not-found-page","path":"/docs/3.x/frontend/not-found-page","sidebar":"someSidebar"},{"id":"frontend/nuxt.js","path":"/docs/3.x/frontend/nuxt.js","sidebar":"someSidebar"},{"id":"frontend/server-side-rendering","path":"/docs/3.x/frontend/server-side-rendering","sidebar":"someSidebar"},{"id":"frontend/single-page-applications","path":"/docs/3.x/frontend/single-page-applications","sidebar":"someSidebar"},{"id":"README","path":"/docs/3.x/","sidebar":"someSidebar"},{"id":"security/body-size-limiting","path":"/docs/3.x/security/body-size-limiting","sidebar":"someSidebar"},{"id":"security/cors","path":"/docs/3.x/security/cors","sidebar":"someSidebar"},{"id":"security/csrf-protection","path":"/docs/3.x/security/csrf-protection","sidebar":"someSidebar"},{"id":"security/http-headers-protection","path":"/docs/3.x/security/http-headers-protection","sidebar":"someSidebar"},{"id":"security/rate-limiting","path":"/docs/3.x/security/rate-limiting","sidebar":"someSidebar"},{"id":"testing/e2e-testing","path":"/docs/3.x/testing/e2e-testing","sidebar":"someSidebar"},{"id":"testing/introduction","path":"/docs/3.x/testing/introduction","sidebar":"someSidebar"},{"id":"testing/unit-testing","path":"/docs/3.x/testing/unit-testing","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-1-introduction","path":"/docs/3.x/tutorials/real-world-example-with-react/1-introduction","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","path":"/docs/3.x/tutorials/real-world-example-with-react/10-auth-with-react","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-11-sign-up","path":"/docs/3.x/tutorials/real-world-example-with-react/11-sign-up","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-12-file-upload","path":"/docs/3.x/tutorials/real-world-example-with-react/12-file-upload","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-13-csrf","path":"/docs/3.x/tutorials/real-world-example-with-react/13-csrf","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-14-production-build","path":"/docs/3.x/tutorials/real-world-example-with-react/14-production-build","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-15-social-auth","path":"/docs/3.x/tutorials/real-world-example-with-react/15-social-auth","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-2-database-set-up","path":"/docs/3.x/tutorials/real-world-example-with-react/2-database-set-up","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-3-the-models","path":"/docs/3.x/tutorials/real-world-example-with-react/3-the-models","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","path":"/docs/3.x/tutorials/real-world-example-with-react/4-the-shell-scripts","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-5-our-first-route","path":"/docs/3.x/tutorials/real-world-example-with-react/5-our-first-route","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","path":"/docs/3.x/tutorials/real-world-example-with-react/6-swagger-interface","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-7-add-frontend","path":"/docs/3.x/tutorials/real-world-example-with-react/7-add-frontend","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-8-authentication","path":"/docs/3.x/tutorials/real-world-example-with-react/8-authentication","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","path":"/docs/3.x/tutorials/real-world-example-with-react/9-authenticated-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/installation-troubleshooting","path":"/docs/3.x/tutorials/simple-todo-list/installation-troubleshooting"},{"id":"tutorials/simple-todo-list/tuto-1-installation","path":"/docs/3.x/tutorials/simple-todo-list/1-installation","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-2-introduction","path":"/docs/3.x/tutorials/simple-todo-list/2-introduction","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-3-the-todo-model","path":"/docs/3.x/tutorials/simple-todo-list/3-the-todo-model","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","path":"/docs/3.x/tutorials/simple-todo-list/4-the-shell-script-create-todo","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-5-the-rest-api","path":"/docs/3.x/tutorials/simple-todo-list/5-the-rest-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","path":"/docs/3.x/tutorials/simple-todo-list/6-validation-and-sanitization","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-7-unit-testing","path":"/docs/3.x/tutorials/simple-todo-list/7-unit-testing","sidebar":"someSidebar"},{"id":"upgrade-to-v3/README","path":"/docs/3.x/upgrade-to-v3/","sidebar":"someSidebar"}],"draftIds":[],"sidebars":{"someSidebar":{"link":{"path":"/docs/3.x/","label":"README"}}}},{"name":"2.x","label":"v2","isLast":false,"path":"/docs/2.x","mainDocId":"README","docs":[{"id":"api-section/graphql","path":"/docs/2.x/api-section/graphql","sidebar":"someSidebar"},{"id":"api-section/gRPC","path":"/docs/2.x/api-section/gRPC","sidebar":"someSidebar"},{"id":"api-section/openapi-and-swagger-ui","path":"/docs/2.x/api-section/openapi-and-swagger-ui","sidebar":"someSidebar"},{"id":"api-section/public-api-and-cors-requests","path":"/docs/2.x/api-section/public-api-and-cors-requests","sidebar":"someSidebar"},{"id":"api-section/rest-blueprints","path":"/docs/2.x/api-section/rest-blueprints","sidebar":"someSidebar"},{"id":"architecture/architecture-overview","path":"/docs/2.x/architecture/architecture-overview","sidebar":"someSidebar"},{"id":"architecture/configuration","path":"/docs/2.x/architecture/configuration","sidebar":"someSidebar"},{"id":"architecture/controllers","path":"/docs/2.x/architecture/controllers","sidebar":"someSidebar"},{"id":"architecture/error-handling","path":"/docs/2.x/architecture/error-handling","sidebar":"someSidebar"},{"id":"architecture/hooks","path":"/docs/2.x/architecture/hooks","sidebar":"someSidebar"},{"id":"architecture/initialization","path":"/docs/2.x/architecture/initialization","sidebar":"someSidebar"},{"id":"architecture/services-and-dependency-injection","path":"/docs/2.x/architecture/services-and-dependency-injection","sidebar":"someSidebar"},{"id":"authentication-and-access-control/administrators-and-roles","path":"/docs/2.x/authentication-and-access-control/administrators-and-roles","sidebar":"someSidebar"},{"id":"authentication-and-access-control/groups-and-permissions","path":"/docs/2.x/authentication-and-access-control/groups-and-permissions","sidebar":"someSidebar"},{"id":"authentication-and-access-control/jwt","path":"/docs/2.x/authentication-and-access-control/jwt","sidebar":"someSidebar"},{"id":"authentication-and-access-control/password-management","path":"/docs/2.x/authentication-and-access-control/password-management","sidebar":"someSidebar"},{"id":"authentication-and-access-control/quick-start","path":"/docs/2.x/authentication-and-access-control/quick-start","sidebar":"someSidebar"},{"id":"authentication-and-access-control/session-tokens","path":"/docs/2.x/authentication-and-access-control/session-tokens","sidebar":"someSidebar"},{"id":"authentication-and-access-control/social-auth","path":"/docs/2.x/authentication-and-access-control/social-auth","sidebar":"someSidebar"},{"id":"authentication-and-access-control/user-class","path":"/docs/2.x/authentication-and-access-control/user-class","sidebar":"someSidebar"},{"id":"common/conversions","path":"/docs/2.x/common/conversions","sidebar":"someSidebar"},{"id":"common/generate-tokens","path":"/docs/2.x/common/generate-tokens","sidebar":"someSidebar"},{"id":"common/logging-and-debugging","path":"/docs/2.x/common/logging-and-debugging","sidebar":"someSidebar"},{"id":"common/serializing-and-deserializing","path":"/docs/2.x/common/serializing-and-deserializing","sidebar":"someSidebar"},{"id":"common/templating","path":"/docs/2.x/common/templating","sidebar":"someSidebar"},{"id":"common/validation-and-sanitization","path":"/docs/2.x/common/validation-and-sanitization","sidebar":"someSidebar"},{"id":"community/awesome-foal","path":"/docs/2.x/community/awesome-foal","sidebar":"someSidebar"},{"id":"comparison-with-other-frameworks/express-fastify","path":"/docs/2.x/comparison-with-other-frameworks/express-fastify","sidebar":"someSidebar"},{"id":"cookbook/expressjs","path":"/docs/2.x/cookbook/expressjs","sidebar":"someSidebar"},{"id":"cookbook/limit-repeated-requests","path":"/docs/2.x/cookbook/limit-repeated-requests","sidebar":"someSidebar"},{"id":"cookbook/not-found-page","path":"/docs/2.x/cookbook/not-found-page","sidebar":"someSidebar"},{"id":"cookbook/request-body-size","path":"/docs/2.x/cookbook/request-body-size","sidebar":"someSidebar"},{"id":"cookbook/root-imports","path":"/docs/2.x/cookbook/root-imports","sidebar":"someSidebar"},{"id":"cookbook/scheduling-jobs","path":"/docs/2.x/cookbook/scheduling-jobs","sidebar":"someSidebar"},{"id":"databases/create-models-and-queries","path":"/docs/2.x/databases/create-models-and-queries","sidebar":"someSidebar"},{"id":"databases/generate-and-run-migrations","path":"/docs/2.x/databases/generate-and-run-migrations","sidebar":"someSidebar"},{"id":"databases/mongodb","path":"/docs/2.x/databases/mongodb","sidebar":"someSidebar"},{"id":"databases/typeorm","path":"/docs/2.x/databases/typeorm","sidebar":"someSidebar"},{"id":"databases/using-another-orm","path":"/docs/2.x/databases/using-another-orm","sidebar":"someSidebar"},{"id":"deployment-and-environments/checklist","path":"/docs/2.x/deployment-and-environments/checklist","sidebar":"someSidebar"},{"id":"development-environment/build-and-start-the-app","path":"/docs/2.x/development-environment/build-and-start-the-app","sidebar":"someSidebar"},{"id":"development-environment/code-generation","path":"/docs/2.x/development-environment/code-generation","sidebar":"someSidebar"},{"id":"development-environment/create-and-run-scripts","path":"/docs/2.x/development-environment/create-and-run-scripts","sidebar":"someSidebar"},{"id":"development-environment/linting-and-code-style","path":"/docs/2.x/development-environment/linting-and-code-style","sidebar":"someSidebar"},{"id":"development-environment/vscode","path":"/docs/2.x/development-environment/vscode","sidebar":"someSidebar"},{"id":"file-system/local-and-cloud-storage","path":"/docs/2.x/file-system/local-and-cloud-storage","sidebar":"someSidebar"},{"id":"file-system/upload-and-download-files","path":"/docs/2.x/file-system/upload-and-download-files","sidebar":"someSidebar"},{"id":"frontend-integration/angular-react-vue","path":"/docs/2.x/frontend-integration/angular-react-vue","sidebar":"someSidebar"},{"id":"frontend-integration/jsx-server-side-rendering","path":"/docs/2.x/frontend-integration/jsx-server-side-rendering","sidebar":"someSidebar"},{"id":"frontend-integration/nuxt.js","path":"/docs/2.x/frontend-integration/nuxt.js","sidebar":"someSidebar"},{"id":"frontend-integration/single-page-applications","path":"/docs/2.x/frontend-integration/single-page-applications","sidebar":"someSidebar"},{"id":"README","path":"/docs/2.x/","sidebar":"someSidebar"},{"id":"security/csrf-protection","path":"/docs/2.x/security/csrf-protection","sidebar":"someSidebar"},{"id":"security/http-headers-protection","path":"/docs/2.x/security/http-headers-protection","sidebar":"someSidebar"},{"id":"security/xss-protection","path":"/docs/2.x/security/xss-protection","sidebar":"someSidebar"},{"id":"testing/e2e-testing","path":"/docs/2.x/testing/e2e-testing","sidebar":"someSidebar"},{"id":"testing/introduction","path":"/docs/2.x/testing/introduction","sidebar":"someSidebar"},{"id":"testing/unit-testing","path":"/docs/2.x/testing/unit-testing","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-1-introduction","path":"/docs/2.x/tutorials/real-world-example-with-react/1-introduction","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-10-auth-with-react","path":"/docs/2.x/tutorials/real-world-example-with-react/10-auth-with-react","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-11-sign-up","path":"/docs/2.x/tutorials/real-world-example-with-react/11-sign-up","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-12-file-upload","path":"/docs/2.x/tutorials/real-world-example-with-react/12-file-upload","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-13-csrf","path":"/docs/2.x/tutorials/real-world-example-with-react/13-csrf","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-14-production-build","path":"/docs/2.x/tutorials/real-world-example-with-react/14-production-build","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-15-social-auth","path":"/docs/2.x/tutorials/real-world-example-with-react/15-social-auth","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-2-database-set-up","path":"/docs/2.x/tutorials/real-world-example-with-react/2-database-set-up","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-3-the-models","path":"/docs/2.x/tutorials/real-world-example-with-react/3-the-models","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-4-the-shell-scripts","path":"/docs/2.x/tutorials/real-world-example-with-react/4-the-shell-scripts","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-5-our-first-route","path":"/docs/2.x/tutorials/real-world-example-with-react/5-our-first-route","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-6-swagger-interface","path":"/docs/2.x/tutorials/real-world-example-with-react/6-swagger-interface","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-7-add-frontend","path":"/docs/2.x/tutorials/real-world-example-with-react/7-add-frontend","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-8-authentication","path":"/docs/2.x/tutorials/real-world-example-with-react/8-authentication","sidebar":"someSidebar"},{"id":"tutorials/real-world-example-with-react/tuto-9-authenticated-api","path":"/docs/2.x/tutorials/real-world-example-with-react/9-authenticated-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/installation-troubleshooting","path":"/docs/2.x/tutorials/simple-todo-list/installation-troubleshooting"},{"id":"tutorials/simple-todo-list/tuto-1-installation","path":"/docs/2.x/tutorials/simple-todo-list/1-installation","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-2-introduction","path":"/docs/2.x/tutorials/simple-todo-list/2-introduction","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-3-the-todo-model","path":"/docs/2.x/tutorials/simple-todo-list/3-the-todo-model","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","path":"/docs/2.x/tutorials/simple-todo-list/4-the-shell-script-create-todo","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-5-the-rest-api","path":"/docs/2.x/tutorials/simple-todo-list/5-the-rest-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","path":"/docs/2.x/tutorials/simple-todo-list/6-validation-and-sanitization","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-7-unit-testing","path":"/docs/2.x/tutorials/simple-todo-list/7-unit-testing","sidebar":"someSidebar"},{"id":"upgrade-to-v2/application-creation","path":"/docs/2.x/upgrade-to-v2/application-creation"},{"id":"upgrade-to-v2/cli-commands","path":"/docs/2.x/upgrade-to-v2/cli-commands"},{"id":"upgrade-to-v2/config-system","path":"/docs/2.x/upgrade-to-v2/config-system"},{"id":"upgrade-to-v2/custom-express-instance","path":"/docs/2.x/upgrade-to-v2/custom-express-instance"},{"id":"upgrade-to-v2/error-handling","path":"/docs/2.x/upgrade-to-v2/error-handling"},{"id":"upgrade-to-v2/file-upload-and-download","path":"/docs/2.x/upgrade-to-v2/file-upload-and-download"},{"id":"upgrade-to-v2/jwt-and-csrf","path":"/docs/2.x/upgrade-to-v2/jwt-and-csrf"},{"id":"upgrade-to-v2/mongodb","path":"/docs/2.x/upgrade-to-v2/mongodb"},{"id":"upgrade-to-v2/openapi","path":"/docs/2.x/upgrade-to-v2/openapi"},{"id":"upgrade-to-v2/README","path":"/docs/2.x/upgrade-to-v2/","sidebar":"someSidebar"},{"id":"upgrade-to-v2/service-and-app-initialization","path":"/docs/2.x/upgrade-to-v2/service-and-app-initialization"},{"id":"upgrade-to-v2/session-tokens","path":"/docs/2.x/upgrade-to-v2/session-tokens"},{"id":"upgrade-to-v2/template-engine","path":"/docs/2.x/upgrade-to-v2/template-engine"},{"id":"upgrade-to-v2/validation-hooks","path":"/docs/2.x/upgrade-to-v2/validation-hooks"},{"id":"websockets","path":"/docs/2.x/websockets","sidebar":"someSidebar"}],"draftIds":[],"sidebars":{"someSidebar":{"link":{"path":"/docs/2.x/","label":"README"}}}},{"name":"1.x","label":"v1","isLast":false,"path":"/docs/1.x","mainDocId":"README","docs":[{"id":"api-section/graphql","path":"/docs/1.x/api-section/graphql","sidebar":"someSidebar"},{"id":"api-section/openapi-and-swagger-ui","path":"/docs/1.x/api-section/openapi-and-swagger-ui","sidebar":"someSidebar"},{"id":"api-section/public-api-and-cors-requests","path":"/docs/1.x/api-section/public-api-and-cors-requests","sidebar":"someSidebar"},{"id":"api-section/rest-blueprints","path":"/docs/1.x/api-section/rest-blueprints","sidebar":"someSidebar"},{"id":"architecture/architecture-overview","path":"/docs/1.x/architecture/architecture-overview","sidebar":"someSidebar"},{"id":"architecture/controllers","path":"/docs/1.x/architecture/controllers","sidebar":"someSidebar"},{"id":"architecture/hooks","path":"/docs/1.x/architecture/hooks","sidebar":"someSidebar"},{"id":"architecture/initialization","path":"/docs/1.x/architecture/initialization","sidebar":"someSidebar"},{"id":"architecture/services-and-dependency-injection","path":"/docs/1.x/architecture/services-and-dependency-injection","sidebar":"someSidebar"},{"id":"authentication-and-access-control/administrators-and-roles","path":"/docs/1.x/authentication-and-access-control/administrators-and-roles","sidebar":"someSidebar"},{"id":"authentication-and-access-control/groups-and-permissions","path":"/docs/1.x/authentication-and-access-control/groups-and-permissions","sidebar":"someSidebar"},{"id":"authentication-and-access-control/jwt","path":"/docs/1.x/authentication-and-access-control/jwt","sidebar":"someSidebar"},{"id":"authentication-and-access-control/password-management","path":"/docs/1.x/authentication-and-access-control/password-management","sidebar":"someSidebar"},{"id":"authentication-and-access-control/quick-start","path":"/docs/1.x/authentication-and-access-control/quick-start","sidebar":"someSidebar"},{"id":"authentication-and-access-control/session-tokens","path":"/docs/1.x/authentication-and-access-control/session-tokens","sidebar":"someSidebar"},{"id":"authentication-and-access-control/social-auth","path":"/docs/1.x/authentication-and-access-control/social-auth","sidebar":"someSidebar"},{"id":"authentication-and-access-control/user-class","path":"/docs/1.x/authentication-and-access-control/user-class","sidebar":"someSidebar"},{"id":"cloud/aws-beanstalk","path":"/docs/1.x/cloud/aws-beanstalk","sidebar":"someSidebar"},{"id":"cloud/firebase","path":"/docs/1.x/cloud/firebase","sidebar":"someSidebar"},{"id":"cookbook/error-handling","path":"/docs/1.x/cookbook/error-handling","sidebar":"someSidebar"},{"id":"cookbook/expressjs","path":"/docs/1.x/cookbook/expressjs","sidebar":"someSidebar"},{"id":"cookbook/generate-tokens","path":"/docs/1.x/cookbook/generate-tokens","sidebar":"someSidebar"},{"id":"cookbook/limit-repeated-requests","path":"/docs/1.x/cookbook/limit-repeated-requests","sidebar":"someSidebar"},{"id":"cookbook/not-found-page","path":"/docs/1.x/cookbook/not-found-page","sidebar":"someSidebar"},{"id":"cookbook/request-body-size","path":"/docs/1.x/cookbook/request-body-size","sidebar":"someSidebar"},{"id":"cookbook/root-imports","path":"/docs/1.x/cookbook/root-imports","sidebar":"someSidebar"},{"id":"cookbook/scheduling-jobs","path":"/docs/1.x/cookbook/scheduling-jobs","sidebar":"someSidebar"},{"id":"databases/create-models-and-queries","path":"/docs/1.x/databases/create-models-and-queries","sidebar":"someSidebar"},{"id":"databases/generate-and-run-migrations","path":"/docs/1.x/databases/generate-and-run-migrations","sidebar":"someSidebar"},{"id":"databases/mongodb","path":"/docs/1.x/databases/mongodb","sidebar":"someSidebar"},{"id":"databases/typeorm","path":"/docs/1.x/databases/typeorm","sidebar":"someSidebar"},{"id":"databases/using-another-orm","path":"/docs/1.x/databases/using-another-orm","sidebar":"someSidebar"},{"id":"deployment-and-environments/configuration","path":"/docs/1.x/deployment-and-environments/configuration","sidebar":"someSidebar"},{"id":"deployment-and-environments/ship-to-production","path":"/docs/1.x/deployment-and-environments/ship-to-production","sidebar":"someSidebar"},{"id":"development-environment/build-and-start-the-app","path":"/docs/1.x/development-environment/build-and-start-the-app","sidebar":"someSidebar"},{"id":"development-environment/code-generation","path":"/docs/1.x/development-environment/code-generation","sidebar":"someSidebar"},{"id":"development-environment/create-and-run-scripts","path":"/docs/1.x/development-environment/create-and-run-scripts","sidebar":"someSidebar"},{"id":"development-environment/linting-and-code-style","path":"/docs/1.x/development-environment/linting-and-code-style","sidebar":"someSidebar"},{"id":"development-environment/vscode","path":"/docs/1.x/development-environment/vscode","sidebar":"someSidebar"},{"id":"file-system/local-and-cloud-storage","path":"/docs/1.x/file-system/local-and-cloud-storage","sidebar":"someSidebar"},{"id":"file-system/upload-and-download-files","path":"/docs/1.x/file-system/upload-and-download-files","sidebar":"someSidebar"},{"id":"frontend-integration/angular-react-vue","path":"/docs/1.x/frontend-integration/angular-react-vue","sidebar":"someSidebar"},{"id":"frontend-integration/jsx-server-side-rendering","path":"/docs/1.x/frontend-integration/jsx-server-side-rendering","sidebar":"someSidebar"},{"id":"frontend-integration/nuxt.js","path":"/docs/1.x/frontend-integration/nuxt.js","sidebar":"someSidebar"},{"id":"frontend-integration/single-page-applications","path":"/docs/1.x/frontend-integration/single-page-applications","sidebar":"someSidebar"},{"id":"README","path":"/docs/1.x/","sidebar":"someSidebar"},{"id":"security/csrf-protection","path":"/docs/1.x/security/csrf-protection","sidebar":"someSidebar"},{"id":"security/http-headers-protection","path":"/docs/1.x/security/http-headers-protection","sidebar":"someSidebar"},{"id":"security/xss-protection","path":"/docs/1.x/security/xss-protection","sidebar":"someSidebar"},{"id":"serializing-and-deserializing","path":"/docs/1.x/serializing-and-deserializing","sidebar":"someSidebar"},{"id":"testing/e2e-testing","path":"/docs/1.x/testing/e2e-testing","sidebar":"someSidebar"},{"id":"testing/introduction","path":"/docs/1.x/testing/introduction","sidebar":"someSidebar"},{"id":"testing/unit-testing","path":"/docs/1.x/testing/unit-testing","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-1-installation","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-1-installation","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-2-introduction","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-2-introduction","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-3-the-todo-model","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-3-the-todo-model","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-4-the-shell-script-create-todo","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-4-the-shell-script-create-todo","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-5-the-rest-api","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-5-the-rest-api","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-6-validation-and-sanitization","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-6-validation-and-sanitization","sidebar":"someSidebar"},{"id":"tutorials/mongodb-todo-list/tuto-7-unit-testing","path":"/docs/1.x/tutorials/mongodb-todo-list/tuto-7-unit-testing","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-1-Introduction","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-1-Introduction","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-2-the-user-and-todo-models","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-2-the-user-and-todo-models","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-3-the-shell-scripts","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-3-the-shell-scripts","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-5-auth-controllers-and-hooks","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-5-auth-controllers-and-hooks","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-6-todos-and-ownership","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-6-todos-and-ownership","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-7-the-signup-page","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-7-the-signup-page","sidebar":"someSidebar"},{"id":"tutorials/multi-user-todo-list/tuto-8-e2e-testing-and-authentication","path":"/docs/1.x/tutorials/multi-user-todo-list/tuto-8-e2e-testing-and-authentication","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-1-installation","path":"/docs/1.x/tutorials/simple-todo-list/tuto-1-installation","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-2-introduction","path":"/docs/1.x/tutorials/simple-todo-list/tuto-2-introduction","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-3-the-todo-model","path":"/docs/1.x/tutorials/simple-todo-list/tuto-3-the-todo-model","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","path":"/docs/1.x/tutorials/simple-todo-list/tuto-4-the-shell-script-create-todo","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-5-the-rest-api","path":"/docs/1.x/tutorials/simple-todo-list/tuto-5-the-rest-api","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-6-validation-and-sanitization","path":"/docs/1.x/tutorials/simple-todo-list/tuto-6-validation-and-sanitization","sidebar":"someSidebar"},{"id":"tutorials/simple-todo-list/tuto-7-unit-testing","path":"/docs/1.x/tutorials/simple-todo-list/tuto-7-unit-testing","sidebar":"someSidebar"},{"id":"utilities/logging-and-debugging","path":"/docs/1.x/utilities/logging-and-debugging","sidebar":"someSidebar"},{"id":"utilities/templating","path":"/docs/1.x/utilities/templating","sidebar":"someSidebar"},{"id":"validation-and-sanitization","path":"/docs/1.x/validation-and-sanitization","sidebar":"someSidebar"}],"draftIds":[],"sidebars":{"someSidebar":{"link":{"path":"/docs/1.x/","label":"README"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en","fr","es","id"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"EN","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"},"fr":{"label":"FR","direction":"ltr","htmlLang":"fr","calendar":"gregory","path":"fr"},"es":{"label":"ES","direction":"ltr","htmlLang":"es","calendar":"gregory","path":"es"},"id":{"label":"Bahasa Indonesia","direction":"ltr","htmlLang":"id","calendar":"gregory","path":"id"}}}');var s=n(22654);const l=JSON.parse('{"docusaurusVersion":"3.1.1","siteVersion":"0.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.1.1"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.1.1"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.1.1"},"docusaurus-plugin-google-analytics":{"type":"package","name":"@docusaurus/plugin-google-analytics","version":"3.1.1"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.1.1"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.1.1"},"docusaurus-theme-search-algolia":{"type":"package","name":"@docusaurus/theme-search-algolia","version":"3.1.1"},"docusaurus-plugin-sass":{"type":"package","name":"docusaurus-plugin-sass","version":"0.2.5"},"docusaurus-tailwindcss":{"type":"local"}}}');var c=n(74848);const d={siteConfig:r.default,siteMetadata:l,globalData:a,i18n:i,codeTranslations:s},u=o.createContext(d);function p(e){let{children:t}=e;return(0,c.jsx)(u.Provider,{value:d,children:t})}},67489:(e,t,n)=>{"use strict";n.d(t,{A:()=>m});var o=n(96540),r=n(38193),a=n(5260),i=n(70440),s=n(79201),l=n(74848);function c(e){let{error:t,tryAgain:n}=e;return(0,l.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,l.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,l.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,l.jsx)(d,{error:t})]})}function d(e){let{error:t}=e;const n=(0,i.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,l.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function u(e){let{error:t,tryAgain:n}=e;return(0,l.jsxs)(m,{fallback:()=>(0,l.jsx)(c,{error:t,tryAgain:n}),children:[(0,l.jsx)(a.A,{children:(0,l.jsx)("title",{children:"Page Error"})}),(0,l.jsx)(s.A,{children:(0,l.jsx)(c,{error:t,tryAgain:n})})]})}const p=e=>(0,l.jsx)(u,{...e});class m extends o.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){r.A.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??p)(e)}return e??null}}},38193:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});const o="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,r={canUseDOM:o,canUseEventListeners:o&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:o&&"IntersectionObserver"in window,canUseViewport:o&&"screen"in window}},5260:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(96540);var o=n(80545),r=n(74848);function a(e){return(0,r.jsx)(o.mg,{...e})}},28774:(e,t,n)=>{"use strict";n.d(t,{A:()=>m});var o=n(96540),r=n(54625),a=n(70440),i=n(44586),s=n(16654),l=n(38193),c=n(63427),d=n(86025),u=n(74848);function p(e,t){let{isNavLink:n,to:p,href:m,activeClassName:f,isActive:h,"data-noBrokenLinkCheck":b,autoAddBaseUrl:g=!0,...v}=e;const{siteConfig:{trailingSlash:x,baseUrl:y}}=(0,i.A)(),{withBaseUrl:w}=(0,d.h)(),k=(0,c.A)(),S=(0,o.useRef)(null);(0,o.useImperativeHandle)(t,(()=>S.current));const _=p||m;const E=(0,s.A)(_),C=_?.replace("pathname://","");let T=void 0!==C?(A=C,g&&(e=>e.startsWith("/"))(A)?w(A):A):void 0;var A;T&&E&&(T=(0,a.applyTrailingSlash)(T,{trailingSlash:x,baseUrl:y}));const j=(0,o.useRef)(!1),R=n?r.k2:r.N_,P=l.A.canUseIntersectionObserver,L=(0,o.useRef)(),N=()=>{j.current||null==T||(window.docusaurus.preload(T),j.current=!0)};(0,o.useEffect)((()=>(!P&&E&&null!=T&&window.docusaurus.prefetch(T),()=>{P&&L.current&&L.current.disconnect()})),[L,T,P,E]);const O=T?.startsWith("#")??!1,D=!v.target||"_self"===v.target,z=!T||!E||!D||O;return b||!O&&z||k.collectLink(T),v.id&&k.collectAnchor(v.id),z?(0,u.jsx)("a",{ref:S,href:T,..._&&!E&&{target:"_blank",rel:"noopener noreferrer"},...v}):(0,u.jsx)(R,{...v,onMouseEnter:N,onTouchStart:N,innerRef:e=>{S.current=e,P&&e&&E&&(L.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(L.current.unobserve(e),L.current.disconnect(),null!=T&&window.docusaurus.prefetch(T))}))})),L.current.observe(e))},to:T,...n&&{isActive:h,activeClassName:f}})}const m=o.forwardRef(p)},21312:(e,t,n)=>{"use strict";n.d(t,{A:()=>c,T:()=>l});var o=n(96540),r=n(74848);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,o.isValidElement)(e)))?n.map(((e,t)=>(0,o.isValidElement)(e)?o.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var i=n(22654);function s(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[t??n]??n??t}function l(e,t){let{message:n,id:o}=e;return a(s({message:n,id:o}),t)}function c(e){let{children:t,id:n,values:o}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const i=s({message:t,id:n});return(0,r.jsx)(r.Fragment,{children:a(i,o)})}},17065:(e,t,n)=>{"use strict";n.d(t,{W:()=>o});const o="default"},16654:(e,t,n)=>{"use strict";function o(e){return/^(?:\w*:|\/\/)/.test(e)}function r(e){return void 0!==e&&!o(e)}n.d(t,{A:()=>r,z:()=>o})},86025:(e,t,n)=>{"use strict";n.d(t,{A:()=>s,h:()=>i});var o=n(96540),r=n(44586),a=n(16654);function i(){const{siteConfig:{baseUrl:e,url:t}}=(0,r.A)(),n=(0,o.useCallback)(((n,o)=>function(e,t,n,o){let{forcePrependBaseUrl:r=!1,absolute:i=!1}=void 0===o?{}:o;if(!n||n.startsWith("#")||(0,a.z)(n))return n;if(r)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const s=n.startsWith(t)?n:t+n.replace(/^\//,"");return i?e+s:s}(t,e,n,o)),[t,e]);return{withBaseUrl:n}}function s(e,t){void 0===t&&(t={});const{withBaseUrl:n}=i();return n(e,t)}},63427:(e,t,n)=>{"use strict";n.d(t,{A:()=>i});var o=n(96540);n(74848);const r=o.createContext({collectAnchor:()=>{},collectLink:()=>{}}),a=()=>(0,o.useContext)(r);function i(){return a()}},44586:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var o=n(96540),r=n(26988);function a(){return(0,o.useContext)(r.o)}},92303:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});var o=n(96540),r=n(6125);function a(){return(0,o.useContext)(r.o)}},205:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});var o=n(96540);const r=n(38193).A.canUseDOM?o.useLayoutEffect:o.useEffect},86921:(e,t,n)=>{"use strict";n.d(t,{A:()=>r});const o=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function r(e){const t={};return function e(n,r){Object.entries(n).forEach((n=>{let[a,i]=n;const s=r?`${r}.${a}`:a;o(i)?e(i,s):t[s]=i}))}(e),t}},53102:(e,t,n)=>{"use strict";n.d(t,{W:()=>i,o:()=>a});var o=n(96540),r=n(74848);const a=o.createContext(null);function i(e){let{children:t,value:n}=e;const i=o.useContext(a),s=(0,o.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const o={...t.data,...n?.data};return{plugin:t.plugin,data:o}}({parent:i,value:n})),[i,n]);return(0,r.jsx)(a.Provider,{value:s,children:t})}},44070:(e,t,n)=>{"use strict";n.d(t,{zK:()=>g,vT:()=>m,gk:()=>f,Gy:()=>u,HW:()=>v,ht:()=>p,r7:()=>b,jh:()=>h});var o=n(56347),r=n(44586),a=n(17065);function i(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,r.A)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const s=e=>e.versions.find((e=>e.isLast));function l(e,t){const n=s(e);return[...e.versions.filter((e=>e!==n)),n].find((e=>!!(0,o.B6)(t,{path:e.path,exact:!1,strict:!1})))}function c(e,t){const n=l(e,t),r=n?.docs.find((e=>!!(0,o.B6)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:r,alternateDocVersions:r?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((o=>{o.id===t&&(n[e.name]=o)}))})),n}(r.id):{}}}const d={},u=()=>i("docusaurus-plugin-content-docs")??d,p=e=>function(e,t,n){void 0===t&&(t=a.W),void 0===n&&(n={});const o=i(e),r=o?.[t];if(!r&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return r}("docusaurus-plugin-content-docs",e,{failfast:!0});function m(e){void 0===e&&(e={});const t=u(),{pathname:n}=(0,o.zy)();return function(e,t,n){void 0===n&&(n={});const r=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,o.B6)(t,{path:n.path,exact:!1,strict:!1})})),a=r?{pluginId:r[0],pluginData:r[1]}:void 0;if(!a&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return a}(t,n,e)}function f(e){void 0===e&&(e={});const t=m(e),{pathname:n}=(0,o.zy)();if(!t)return;return{activePlugin:t,activeVersion:l(t.pluginData,n)}}function h(e){return p(e).versions}function b(e){const t=p(e);return s(t)}function g(e){const t=p(e),{pathname:n}=(0,o.zy)();return c(t,n)}function v(e){const t=p(e),{pathname:n}=(0,o.zy)();return function(e,t){const n=s(e);return{latestDocSuggestion:c(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},37651:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});const o={onRouteDidUpdate(e){let{location:t,previousLocation:n}=e;!n||t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash||(window.ga("set","page",t.pathname+t.search+t.hash),window.ga("send","pageview"))}}},76294:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var o=n(5947),r=n.n(o);r().configure({showSpinner:!1});const a={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{r().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){r().done()}}},26134:(e,t,n)=>{"use strict";n.r(t);var o=n(71765),r=n(4784);!function(e){const{themeConfig:{prism:t}}=r.default,{additionalLanguages:o}=t;globalThis.Prism=e,o.forEach((e=>{"php"===e&&n(19700),n(18692)(`./prism-${e}`)})),delete globalThis.Prism}(o.My)},51107:(e,t,n)=>{"use strict";n.d(t,{A:()=>d});n(96540);var o=n(34164),r=n(21312),a=n(6342),i=n(28774),s=n(63427);const l={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var c=n(74848);function d(e){let{as:t,id:n,...d}=e;const u=(0,s.A)(),{navbar:{hideOnScroll:p}}=(0,a.p)();if("h1"===t||!n)return(0,c.jsx)(t,{...d,id:void 0});u.collectAnchor(n);const m=(0,r.T)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof d.children?d.children:n});return(0,c.jsxs)(t,{...d,className:(0,o.A)("anchor",p?l.anchorWithHideOnScrollNavbar:l.anchorWithStickyNavbar,d.className),id:n,children:[d.children,(0,c.jsx)(i.A,{className:"hash-link",to:`#${n}`,"aria-label":m,title:m,children:"\u200b"})]})}},43186:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(96540);const o={iconExternalLink:"iconExternalLink_nPIU"};var r=n(74848);function a(e){let{width:t=13.5,height:n=13.5}=e;return(0,r.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:o.iconExternalLink,children:(0,r.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},79201:(e,t,n)=>{"use strict";n.d(t,{A:()=>Nt});var o=n(96540),r=n(34164),a=n(67489),i=n(69024),s=n(56347),l=n(21312),c=n(75062),d=n(74848);const u="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function m(){const e=(0,o.useRef)(null),{action:t}=(0,s.W6)(),n=(0,o.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(u);t&&p(t)}),[]);return(0,c.$)((n=>{let{location:o}=n;e.current&&!o.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const f=(0,l.T)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??f,{containerRef:n,onClick:o}=m();return(0,d.jsx)("div",{ref:n,role:"region","aria-label":f,children:(0,d.jsx)("a",{...e,href:`#${u}`,onClick:o,children:t})})}var b=n(17559),g=n(14090);const v={skipToContent:"skipToContent_fXgn"};function x(){return(0,d.jsx)(h,{className:v.skipToContent})}var y=n(6342),w=n(65041);function k(e){let{width:t=21,height:n=21,color:o="currentColor",strokeWidth:r=1.2,className:a,...i}=e;return(0,d.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...i,children:(0,d.jsx)("g",{stroke:o,strokeWidth:r,children:(0,d.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const S={closeButton:"closeButton_CVFx"};function _(e){return(0,d.jsx)("button",{type:"button","aria-label":(0,l.T)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,r.A)("clean-btn close",S.closeButton,e.className),children:(0,d.jsx)(k,{width:14,height:14,strokeWidth:3.1})})}const E={content:"content_knG7"};function C(e){const{announcementBar:t}=(0,y.p)(),{content:n}=t;return(0,d.jsx)("div",{...e,className:(0,r.A)(E.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const T={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function A(){const{announcementBar:e}=(0,y.p)(),{isActive:t,close:n}=(0,w.Mj)();if(!t)return null;const{backgroundColor:o,textColor:r,isCloseable:a}=e;return(0,d.jsxs)("div",{className:T.announcementBar,style:{backgroundColor:o,color:r},role:"banner",children:[a&&(0,d.jsx)("div",{className:T.announcementBarPlaceholder}),(0,d.jsx)(C,{className:T.announcementBarContent}),a&&(0,d.jsx)(_,{onClick:n,className:T.announcementBarClose})]})}var j=n(22069),R=n(23104);var P=n(89532),L=n(75600);const N=o.createContext(null);function O(e){let{children:t}=e;const n=function(){const e=(0,j.M)(),t=(0,L.YL)(),[n,r]=(0,o.useState)(!1),a=null!==t.component,i=(0,P.ZC)(a);return(0,o.useEffect)((()=>{a&&!i&&r(!0)}),[a,i]),(0,o.useEffect)((()=>{a?e.shown||r(!0):r(!1)}),[e.shown,a]),(0,o.useMemo)((()=>[n,r]),[n])}();return(0,d.jsx)(N.Provider,{value:n,children:t})}function D(e){if(e.component){const t=e.component;return(0,d.jsx)(t,{...e.props})}}function z(){const e=(0,o.useContext)(N);if(!e)throw new P.dV("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,r=(0,o.useCallback)((()=>n(!1)),[n]),a=(0,L.YL)();return(0,o.useMemo)((()=>({shown:t,hide:r,content:D(a)})),[r,a,t])}function M(e){let{header:t,primaryMenu:n,secondaryMenu:o}=e;const{shown:a}=z();return(0,d.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,d.jsxs)("div",{className:(0,r.A)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":a}),children:[(0,d.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,d.jsx)("div",{className:"navbar-sidebar__item menu",children:o})]})]})}var I=n(95293),F=n(92303);function B(e){return(0,d.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,d.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function q(e){return(0,d.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,d.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const $={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function U(e){let{className:t,buttonClassName:n,value:o,onChange:a}=e;const i=(0,F.A)(),s=(0,l.T)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===o?(0,l.T)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,l.T)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,d.jsx)("div",{className:(0,r.A)($.toggle,t),children:(0,d.jsxs)("button",{className:(0,r.A)("clean-btn",$.toggleButton,!i&&$.toggleButtonDisabled,n),type:"button",onClick:()=>a("dark"===o?"light":"dark"),disabled:!i,title:s,"aria-label":s,"aria-live":"polite",children:[(0,d.jsx)(B,{className:(0,r.A)($.toggleIcon,$.lightToggleIcon)}),(0,d.jsx)(q,{className:(0,r.A)($.toggleIcon,$.darkToggleIcon)})]})})}const H=o.memo(U),G={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function V(e){let{className:t}=e;const n=(0,y.p)().navbar.style,o=(0,y.p)().colorMode.disableSwitch,{colorMode:r,setColorMode:a}=(0,I.G)();return o?null:(0,d.jsx)(H,{className:t,buttonClassName:"dark"===n?G.darkNavbarColorModeToggle:void 0,value:r,onChange:a})}var W=n(23465);function K(){return(0,d.jsx)(W.A,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function Q(){const e=(0,j.M)();return(0,d.jsx)("button",{type:"button","aria-label":(0,l.T)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,d.jsx)(k,{color:"var(--ifm-color-emphasis-600)"})})}function Y(){return(0,d.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,d.jsx)(K,{}),(0,d.jsx)(V,{className:"margin-right--md"}),(0,d.jsx)(Q,{})]})}var Z=n(28774),X=n(86025),J=n(16654),ee=n(91252),te=n(43186);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:o,href:r,label:a,html:i,isDropdownLink:s,prependBaseUrlToHref:l,...c}=e;const u=(0,X.A)(o),p=(0,X.A)(t),m=(0,X.A)(r,{forcePrependBaseUrl:!0}),f=a&&r&&!(0,J.A)(r),h=i?{dangerouslySetInnerHTML:{__html:i}}:{children:(0,d.jsxs)(d.Fragment,{children:[a,f&&(0,d.jsx)(te.A,{...s&&{width:12,height:12}})]})};return r?(0,d.jsx)(Z.A,{href:l?m:r,...c,...h}):(0,d.jsx)(Z.A,{to:u,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?(0,ee.G)(n,t.pathname):t.pathname.startsWith(p)},...c,...h})}function oe(e){let{className:t,isDropdownItem:n=!1,...o}=e;const a=(0,d.jsx)(ne,{className:(0,r.A)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...o});return n?(0,d.jsx)("li",{children:a}):a}function re(e){let{className:t,isDropdownItem:n,...o}=e;return(0,d.jsx)("li",{className:"menu__list-item",children:(0,d.jsx)(ne,{className:(0,r.A)("menu__link",t),...o})})}function ae(e){let{mobile:t=!1,position:n,...o}=e;const r=t?re:oe;return(0,d.jsx)(r,{...o,activeClassName:o.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var ie=n(41422),se=n(99169),le=n(44586);const ce={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function de(e,t){return e.some((e=>function(e,t){return!!(0,se.ys)(e.to,t)||!!(0,ee.G)(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function ue(e){let{items:t,position:n,className:a,onClick:i,...s}=e;const l=(0,o.useRef)(null),[c,u]=(0,o.useState)(!1);return(0,o.useEffect)((()=>{const e=e=>{l.current&&!l.current.contains(e.target)&&u(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[l]),(0,d.jsxs)("div",{ref:l,className:(0,r.A)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":c}),children:[(0,d.jsx)(ne,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:s.to?void 0:"#",className:(0,r.A)("navbar__link",a),...s,onClick:s.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),u(!c))},children:s.children??s.label}),(0,d.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,o.createElement)(Ge,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:a,onClick:i,...l}=e;const c=function(){const{siteConfig:{baseUrl:e}}=(0,le.A)(),{pathname:t}=(0,s.zy)();return t.replace(e,"/")}(),u=de(t,c),{collapsed:p,toggleCollapsed:m,setCollapsed:f}=(0,ie.u)({initialState:()=>!u});return(0,o.useEffect)((()=>{u&&f(!u)}),[c,u,f]),(0,d.jsxs)("li",{className:(0,r.A)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,d.jsx)(ne,{role:"button",className:(0,r.A)(ce.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...l,onClick:e=>{e.preventDefault(),m()},children:l.children??l.label}),(0,d.jsx)(ie.N,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,o.createElement)(Ge,{mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active",...e,key:t})))})]})}function me(e){let{mobile:t=!1,...n}=e;const o=t?pe:ue;return(0,d.jsx)(o,{...n})}var fe=n(32131);function he(e){let{width:t=20,height:n=20,...o}=e;return(0,d.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...o,children:(0,d.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const be="iconLanguage_nlXk";var ge=n(40961);function ve(){return o.createElement("svg",{width:"15",height:"15",className:"DocSearch-Control-Key-Icon"},o.createElement("path",{d:"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953",strokeWidth:"1.2",stroke:"currentColor",fill:"none",strokeLinecap:"square"}))}var xe=n(89188),ye=["translations"];function we(){return we=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,o=new Array(t);n=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var Ee="Ctrl";var Ce=o.forwardRef((function(e,t){var n=e.translations,r=void 0===n?{}:n,a=_e(e,ye),i=r.buttonText,s=void 0===i?"Search":i,l=r.buttonAriaLabel,c=void 0===l?"Search":l,d=ke((0,o.useState)(null),2),u=d[0],p=d[1];return(0,o.useEffect)((function(){"undefined"!=typeof navigator&&(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?p("\u2318"):p(Ee))}),[]),o.createElement("button",we({type:"button",className:"DocSearch DocSearch-Button","aria-label":c},a,{ref:t}),o.createElement("span",{className:"DocSearch-Button-Container"},o.createElement(xe.W,null),o.createElement("span",{className:"DocSearch-Button-Placeholder"},s)),o.createElement("span",{className:"DocSearch-Button-Keys"},null!==u&&o.createElement(o.Fragment,null,o.createElement(Te,{reactsToKey:u===Ee?Ee:"Meta"},u===Ee?o.createElement(ve,null):u),o.createElement(Te,{reactsToKey:"k"},"K"))))}));function Te(e){var t=e.reactsToKey,n=e.children,r=ke((0,o.useState)(!1),2),a=r[0],i=r[1];return(0,o.useEffect)((function(){if(t)return window.addEventListener("keydown",e),window.addEventListener("keyup",n),function(){window.removeEventListener("keydown",e),window.removeEventListener("keyup",n)};function e(e){e.key===t&&i(!0)}function n(e){e.key!==t&&"Meta"!==e.key||i(!1)}}),[t]),o.createElement("kbd",{className:a?"DocSearch-Button-Key DocSearch-Button-Key--pressed":"DocSearch-Button-Key"},n)}var Ae=n(5260),je=n(24255),Re=n(51062),Pe=n(2967);const Le={button:{buttonText:(0,l.T)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"}),buttonAriaLabel:(0,l.T)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"})},modal:{searchBox:{resetButtonTitle:(0,l.T)({id:"theme.SearchModal.searchBox.resetButtonTitle",message:"Clear the query",description:"The label and ARIA label for search box reset button"}),resetButtonAriaLabel:(0,l.T)({id:"theme.SearchModal.searchBox.resetButtonTitle",message:"Clear the query",description:"The label and ARIA label for search box reset button"}),cancelButtonText:(0,l.T)({id:"theme.SearchModal.searchBox.cancelButtonText",message:"Cancel",description:"The label and ARIA label for search box cancel button"}),cancelButtonAriaLabel:(0,l.T)({id:"theme.SearchModal.searchBox.cancelButtonText",message:"Cancel",description:"The label and ARIA label for search box cancel button"})},startScreen:{recentSearchesTitle:(0,l.T)({id:"theme.SearchModal.startScreen.recentSearchesTitle",message:"Recent",description:"The title for recent searches"}),noRecentSearchesText:(0,l.T)({id:"theme.SearchModal.startScreen.noRecentSearchesText",message:"No recent searches",description:"The text when no recent searches"}),saveRecentSearchButtonTitle:(0,l.T)({id:"theme.SearchModal.startScreen.saveRecentSearchButtonTitle",message:"Save this search",description:"The label for save recent search button"}),removeRecentSearchButtonTitle:(0,l.T)({id:"theme.SearchModal.startScreen.removeRecentSearchButtonTitle",message:"Remove this search from history",description:"The label for remove recent search button"}),favoriteSearchesTitle:(0,l.T)({id:"theme.SearchModal.startScreen.favoriteSearchesTitle",message:"Favorite",description:"The title for favorite searches"}),removeFavoriteSearchButtonTitle:(0,l.T)({id:"theme.SearchModal.startScreen.removeFavoriteSearchButtonTitle",message:"Remove this search from favorites",description:"The label for remove favorite search button"})},errorScreen:{titleText:(0,l.T)({id:"theme.SearchModal.errorScreen.titleText",message:"Unable to fetch results",description:"The title for error screen of search modal"}),helpText:(0,l.T)({id:"theme.SearchModal.errorScreen.helpText",message:"You might want to check your network connection.",description:"The help text for error screen of search modal"})},footer:{selectText:(0,l.T)({id:"theme.SearchModal.footer.selectText",message:"to select",description:"The explanatory text of the action for the enter key"}),selectKeyAriaLabel:(0,l.T)({id:"theme.SearchModal.footer.selectKeyAriaLabel",message:"Enter key",description:"The ARIA label for the Enter key button that makes the selection"}),navigateText:(0,l.T)({id:"theme.SearchModal.footer.navigateText",message:"to navigate",description:"The explanatory text of the action for the Arrow up and Arrow down key"}),navigateUpKeyAriaLabel:(0,l.T)({id:"theme.SearchModal.footer.navigateUpKeyAriaLabel",message:"Arrow up",description:"The ARIA label for the Arrow up key button that makes the navigation"}),navigateDownKeyAriaLabel:(0,l.T)({id:"theme.SearchModal.footer.navigateDownKeyAriaLabel",message:"Arrow down",description:"The ARIA label for the Arrow down key button that makes the navigation"}),closeText:(0,l.T)({id:"theme.SearchModal.footer.closeText",message:"to close",description:"The explanatory text of the action for Escape key"}),closeKeyAriaLabel:(0,l.T)({id:"theme.SearchModal.footer.closeKeyAriaLabel",message:"Escape key",description:"The ARIA label for the Escape key button that close the modal"}),searchByText:(0,l.T)({id:"theme.SearchModal.footer.searchByText",message:"Search by",description:"The text explain that the search is making by Algolia"})},noResultsScreen:{noResultsText:(0,l.T)({id:"theme.SearchModal.noResultsScreen.noResultsText",message:"No results for",description:"The text explains that there are no results for the following search"}),suggestedQueryText:(0,l.T)({id:"theme.SearchModal.noResultsScreen.suggestedQueryText",message:"Try searching for",description:"The text for the suggested query when no results are found for the following search"}),reportMissingResultsText:(0,l.T)({id:"theme.SearchModal.noResultsScreen.reportMissingResultsText",message:"Believe this query should return results?",description:"The text for the question where the user thinks there are missing results"}),reportMissingResultsLinkText:(0,l.T)({id:"theme.SearchModal.noResultsScreen.reportMissingResultsLinkText",message:"Let us know.",description:"The text for the link to report missing results"})}},placeholder:(0,l.T)({id:"theme.SearchModal.placeholder",message:"Search docs",description:"The placeholder of the input of the DocSearch pop-up modal"})};let Ne=null;function Oe(e){let{hit:t,children:n}=e;return(0,d.jsx)(Z.A,{to:t.url,children:n})}function De(e){let{state:t,onClose:n}=e;const o=(0,je.w)();return(0,d.jsx)(Z.A,{to:o(t.query),onClick:n,children:(0,d.jsx)(l.A,{id:"theme.SearchBar.seeAll",values:{count:t.context.nbHits},children:"See all {count} results"})})}function ze(e){let{contextualSearch:t,externalUrlRegex:r,...a}=e;const{siteMetadata:i}=(0,le.A)(),l=(0,Re.C)(),c=function(){const{locale:e,tags:t}=(0,Pe.af)();return[`language:${e}`,t.map((e=>`docusaurus_tag:${e}`))]}(),u=a.searchParameters?.facetFilters??[],p=t?function(e,t){const n=e=>"string"==typeof e?[e]:e;return[...n(e),...n(t)]}(c,u):u,m={...a.searchParameters,facetFilters:p},f=(0,s.W6)(),h=(0,o.useRef)(null),b=(0,o.useRef)(null),[g,v]=(0,o.useState)(!1),[x,y]=(0,o.useState)(void 0),w=(0,o.useCallback)((()=>Ne?Promise.resolve():Promise.all([n.e(9462).then(n.bind(n,9462)),Promise.all([n.e(1869),n.e(8913)]).then(n.bind(n,58913)),Promise.all([n.e(1869),n.e(416)]).then(n.bind(n,90416))]).then((e=>{let[{DocSearchModal:t}]=e;Ne=t}))),[]),k=(0,o.useCallback)((()=>{w().then((()=>{h.current=document.createElement("div"),document.body.insertBefore(h.current,document.body.firstChild),v(!0)}))}),[w,v]),S=(0,o.useCallback)((()=>{v(!1),h.current?.remove()}),[v]),_=(0,o.useCallback)((e=>{w().then((()=>{v(!0),y(e.key)}))}),[w,v,y]),E=(0,o.useRef)({navigate(e){let{itemUrl:t}=e;(0,ee.G)(r,t)?window.location.href=t:f.push(t)}}).current,C=(0,o.useRef)((e=>a.transformItems?a.transformItems(e):e.map((e=>({...e,url:l(e.url)}))))).current,T=(0,o.useMemo)((()=>e=>(0,d.jsx)(De,{...e,onClose:S})),[S]),A=(0,o.useCallback)((e=>(e.addAlgoliaAgent("docusaurus",i.docusaurusVersion),e)),[i.docusaurusVersion]);return function(e){var t=e.isOpen,n=e.onOpen,r=e.onClose,a=e.onInput,i=e.searchButtonRef;o.useEffect((function(){function e(e){var o;(27===e.keyCode&&t||"k"===(null===(o=e.key)||void 0===o?void 0:o.toLowerCase())&&(e.metaKey||e.ctrlKey)||!function(e){var t=e.target,n=t.tagName;return t.isContentEditable||"INPUT"===n||"SELECT"===n||"TEXTAREA"===n}(e)&&"/"===e.key&&!t)&&(e.preventDefault(),t?r():document.body.classList.contains("DocSearch--active")||document.body.classList.contains("DocSearch--active")||n()),i&&i.current===document.activeElement&&a&&/[a-zA-Z0-9]/.test(String.fromCharCode(e.keyCode))&&a(e)}return window.addEventListener("keydown",e),function(){window.removeEventListener("keydown",e)}}),[t,n,r,a,i])}({isOpen:g,onOpen:k,onClose:S,onInput:_,searchButtonRef:b}),(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(Ae.A,{children:(0,d.jsx)("link",{rel:"preconnect",href:`https://${a.appId}-dsn.algolia.net`,crossOrigin:"anonymous"})}),(0,d.jsx)(Ce,{onTouchStart:w,onFocus:w,onMouseOver:w,onClick:k,ref:b,translations:Le.button}),g&&Ne&&h.current&&(0,ge.createPortal)((0,d.jsx)(Ne,{onClose:S,initialScrollY:window.scrollY,initialQuery:x,navigator:E,transformItems:C,hitComponent:Oe,transformSearchClient:A,...a.searchPagePath&&{resultsFooterComponent:T},...a,searchParameters:m,placeholder:Le.placeholder,translations:Le.modal}),h.current)]})}function Me(){const{siteConfig:e}=(0,le.A)();return(0,d.jsx)(ze,{...e.themeConfig.algolia})}const Ie={navbarSearchContainer:"navbarSearchContainer_Bca1"};function Fe(e){let{children:t,className:n}=e;return(0,d.jsx)("div",{className:(0,r.A)(n,Ie.navbarSearchContainer),children:t})}var Be=n(44070),qe=n(84142);var $e=n(55597);const Ue=e=>e.docs.find((t=>t.id===e.mainDocId));const He={default:ae,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:o,queryString:r="",...a}=e;const{i18n:{currentLocale:i,locales:c,localeConfigs:u}}=(0,le.A)(),p=(0,fe.o)(),{search:m,hash:f}=(0,s.zy)(),h=[...n,...c.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${m}${f}${r}`;return{label:u[e].label,lang:u[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...o],b=t?(0,l.T)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):u[i].label;return(0,d.jsx)(me,{...a,mobile:t,label:(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(he,{className:be}),b]}),items:h})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,d.jsx)(Fe,{className:n,children:(0,d.jsx)(Me,{})})},dropdown:me,html:function(e){let{value:t,className:n,mobile:o=!1,isDropdownItem:a=!1}=e;const i=a?"li":"div";return(0,d.jsx)(i,{className:(0,r.A)({navbar__item:!o&&!a,"menu__list-item":o},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:o,...r}=e;const{activeDoc:a}=(0,Be.zK)(o),i=(0,qe.QB)(t,o),s=a?.path===i?.path;return null===i||i.unlisted&&!s?null:(0,d.jsx)(ae,{exact:!0,...r,isActive:()=>s||!!a?.sidebar&&a.sidebar===i.sidebar,label:n??i.id,to:i.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:o,...r}=e;const{activeDoc:a}=(0,Be.zK)(o),i=(0,qe.fW)(t,o).link;if(!i)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,d.jsx)(ae,{exact:!0,...r,isActive:()=>a?.sidebar===t,label:n??i.label,to:i.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:o,...r}=e;const a=(0,qe.Vd)(o)[0],i=t??a.label,s=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(a).path;return(0,d.jsx)(ae,{...r,label:i,to:s})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:o,dropdownItemsBefore:r,dropdownItemsAfter:a,...i}=e;const{search:c,hash:u}=(0,s.zy)(),p=(0,Be.zK)(n),m=(0,Be.jh)(n),{savePreferredVersionName:f}=(0,$e.g1)(n),h=[...r,...m.map((e=>{const t=p.alternateDocVersions[e.name]??Ue(e);return{label:e.label,to:`${t.path}${c}${u}`,isActive:()=>e===p.activeVersion,onClick:()=>f(e.name)}})),...a],b=(0,qe.Vd)(n)[0],g=t&&h.length>1?(0,l.T)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):b.label,v=t&&h.length>1?void 0:Ue(b).path;return h.length<=1?(0,d.jsx)(ae,{...i,mobile:t,label:g,to:v,isActive:o?()=>!1:void 0}):(0,d.jsx)(me,{...i,mobile:t,label:g,to:v,items:h,isActive:o?()=>!1:void 0})}};function Ge(e){let{type:t,...n}=e;const o=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),r=He[o];if(!r)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,d.jsx)(r,{...n})}function Ve(){const e=(0,j.M)(),t=(0,y.p)().navbar.items;return(0,d.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,o.createElement)(Ge,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function We(e){return(0,d.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,d.jsx)(l.A,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function Ke(){const e=0===(0,y.p)().navbar.items.length,t=z();return(0,d.jsxs)(d.Fragment,{children:[!e&&(0,d.jsx)(We,{onClick:()=>t.hide()}),t.content]})}function Qe(){const e=(0,j.M)();var t;return void 0===(t=e.shown)&&(t=!0),(0,o.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,d.jsx)(M,{header:(0,d.jsx)(Y,{}),primaryMenu:(0,d.jsx)(Ve,{}),secondaryMenu:(0,d.jsx)(Ke,{})}):null}const Ye={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function Ze(e){return(0,d.jsx)("div",{role:"presentation",...e,className:(0,r.A)("navbar-sidebar__backdrop",e.className)})}function Xe(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:a}}=(0,y.p)(),i=(0,j.M)(),{navbarRef:s,isNavbarVisible:u}=function(e){const[t,n]=(0,o.useState)(e),r=(0,o.useRef)(!1),a=(0,o.useRef)(0),i=(0,o.useCallback)((e=>{null!==e&&(a.current=e.getBoundingClientRect().height)}),[]);return(0,R.Mq)(((t,o)=>{let{scrollY:i}=t;if(!e)return;if(i=s?n(!1):i+c{if(!e)return;const o=t.location.hash;if(o?document.getElementById(o.substring(1)):void 0)return r.current=!0,void n(!1);n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return(0,d.jsxs)("nav",{ref:s,"aria-label":(0,l.T)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,r.A)("navbar","navbar--fixed-top",n&&[Ye.navbarHideable,!u&&Ye.navbarHidden],{"navbar--dark":"dark"===a,"navbar--primary":"primary"===a,"navbar-sidebar--show":i.shown}),children:[t,(0,d.jsx)(Ze,{onClick:i.toggle}),(0,d.jsx)(Qe,{})]})}var Je=n(70440);const et={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function tt(e){return(0,d.jsx)("button",{type:"button",...e,children:(0,d.jsx)(l.A,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function nt(e){let{error:t}=e;const n=(0,Je.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,d.jsx)("p",{className:et.errorBoundaryError,children:n})}class ot extends o.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const rt="right";function at(e){let{width:t=30,height:n=30,className:o,...r}=e;return(0,d.jsx)("svg",{className:o,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...r,children:(0,d.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function it(){const{toggle:e,shown:t}=(0,j.M)();return(0,d.jsx)("button",{onClick:e,"aria-label":(0,l.T)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,d.jsx)(at,{})})}const st={colorModeToggle:"colorModeToggle_DEke"};function lt(e){let{items:t}=e;return(0,d.jsx)(d.Fragment,{children:t.map(((e,t)=>(0,d.jsx)(ot,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,d.jsx)(Ge,{...e})},t)))})}function ct(e){let{left:t,right:n}=e;return(0,d.jsxs)("div",{className:"navbar__inner",children:[(0,d.jsx)("div",{className:"navbar__items",children:t}),(0,d.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function dt(){const e=(0,j.M)(),t=(0,y.p)().navbar.items,[n,o]=function(e){function t(e){return"left"===(e.position??rt)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),r=t.find((e=>"search"===e.type));return(0,d.jsx)(ct,{left:(0,d.jsxs)(d.Fragment,{children:[!e.disabled&&(0,d.jsx)(it,{}),(0,d.jsx)(K,{}),(0,d.jsx)(lt,{items:n})]}),right:(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(lt,{items:o}),(0,d.jsx)(V,{className:st.colorModeToggle}),!r&&(0,d.jsx)(Fe,{children:(0,d.jsx)(Me,{})})]})})}function ut(){return(0,d.jsx)(Xe,{children:(0,d.jsx)(dt,{})})}function pt(e){let{item:t}=e;const{to:n,href:o,label:r,prependBaseUrlToHref:a,...i}=t,s=(0,X.A)(n),l=(0,X.A)(o,{forcePrependBaseUrl:!0});return(0,d.jsxs)(Z.A,{className:"footer__link-item",...o?{href:a?l:o}:{to:s},...i,children:[r,o&&!(0,J.A)(o)&&(0,d.jsx)(te.A,{})]})}function mt(e){let{item:t}=e;return t.html?(0,d.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,d.jsx)("li",{className:"footer__item",children:(0,d.jsx)(pt,{item:t})},t.href??t.to)}function ft(e){let{column:t}=e;return(0,d.jsxs)("div",{className:"col footer__col",children:[(0,d.jsx)("div",{className:"footer__title",children:t.title}),(0,d.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,d.jsx)(mt,{item:e},t)))})]})}function ht(e){let{columns:t}=e;return(0,d.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,d.jsx)(ft,{column:e},t)))})}function bt(){return(0,d.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function gt(e){let{item:t}=e;return t.html?(0,d.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,d.jsx)(pt,{item:t})}function vt(e){let{links:t}=e;return(0,d.jsx)("div",{className:"footer__links text--center",children:(0,d.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,d.jsxs)(o.Fragment,{children:[(0,d.jsx)(gt,{item:e}),t.length!==n+1&&(0,d.jsx)(bt,{})]},n)))})})}function xt(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,d.jsx)(ht,{columns:t}):(0,d.jsx)(vt,{links:t})}var yt=n(21122);const wt={footerLogoLink:"footerLogoLink_BH7S"};function kt(e){let{logo:t}=e;const{withBaseUrl:n}=(0,X.h)(),o={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,d.jsx)(yt.A,{className:(0,r.A)("footer__logo",t.className),alt:t.alt,sources:o,width:t.width,height:t.height,style:t.style})}function St(e){let{logo:t}=e;return t.href?(0,d.jsx)(Z.A,{href:t.href,className:wt.footerLogoLink,target:t.target,children:(0,d.jsx)(kt,{logo:t})}):(0,d.jsx)(kt,{logo:t})}function _t(e){let{copyright:t}=e;return(0,d.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function Et(e){let{style:t,links:n,logo:o,copyright:a}=e;return(0,d.jsx)("footer",{className:(0,r.A)("footer",{"footer--dark":"dark"===t}),children:(0,d.jsxs)("div",{className:"container container-fluid",children:[n,(o||a)&&(0,d.jsxs)("div",{className:"footer__bottom text--center",children:[o&&(0,d.jsx)("div",{className:"margin-bottom--sm",children:o}),a]})]})})}function Ct(){const{footer:e}=(0,y.p)();if(!e)return null;const{copyright:t,links:n,logo:o,style:r}=e;return(0,d.jsx)(Et,{style:r,links:n&&n.length>0&&(0,d.jsx)(xt,{links:n}),logo:o&&(0,d.jsx)(St,{logo:o}),copyright:t&&(0,d.jsx)(_t,{copyright:t})})}const Tt=o.memo(Ct),At=(0,P.fM)([I.a,w.oq,R.Tv,$e.VQ,i.Jx,function(e){let{children:t}=e;return(0,d.jsx)(L.y_,{children:(0,d.jsx)(j.e,{children:(0,d.jsx)(O,{children:t})})})}]);function jt(e){let{children:t}=e;return(0,d.jsx)(At,{children:t})}var Rt=n(51107);function Pt(e){let{error:t,tryAgain:n}=e;return(0,d.jsx)("main",{className:"container margin-vert--xl",children:(0,d.jsx)("div",{className:"row",children:(0,d.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,d.jsx)(Rt.A,{as:"h1",className:"hero__title",children:(0,d.jsx)(l.A,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,d.jsx)("div",{className:"margin-vert--lg",children:(0,d.jsx)(tt,{onClick:n,className:"button button--primary shadow--lw"})}),(0,d.jsx)("hr",{}),(0,d.jsx)("div",{className:"margin-vert--md",children:(0,d.jsx)(nt,{error:t})})]})})})}const Lt={mainWrapper:"mainWrapper_z2l0"};function Nt(e){const{children:t,noFooter:n,wrapperClassName:o,title:s,description:l}=e;return(0,g.J)(),(0,d.jsxs)(jt,{children:[(0,d.jsx)(i.be,{title:s,description:l}),(0,d.jsx)(x,{}),(0,d.jsx)(A,{}),(0,d.jsx)(ut,{}),(0,d.jsx)("div",{id:u,className:(0,r.A)(b.G.wrapper.main,Lt.mainWrapper,o),children:(0,d.jsx)(a.A,{fallback:e=>(0,d.jsx)(Pt,{...e}),children:t})}),!n&&(0,d.jsx)(Tt,{})]})}},23465:(e,t,n)=>{"use strict";n.d(t,{A:()=>d});n(96540);var o=n(28774),r=n(86025),a=n(44586),i=n(6342),s=n(21122),l=n(74848);function c(e){let{logo:t,alt:n,imageClassName:o}=e;const a={light:(0,r.A)(t.src),dark:(0,r.A)(t.srcDark||t.src)},i=(0,l.jsx)(s.A,{className:t.className,sources:a,height:t.height,width:t.width,alt:n,style:t.style});return o?(0,l.jsx)("div",{className:o,children:i}):i}function d(e){const{siteConfig:{title:t}}=(0,a.A)(),{navbar:{title:n,logo:s}}=(0,i.p)(),{imageClassName:d,titleClassName:u,...p}=e,m=(0,r.A)(s?.href||"/"),f=n?"":t,h=s?.alt??f;return(0,l.jsxs)(o.A,{to:m,...p,...s?.target&&{target:s.target},children:[s&&(0,l.jsx)(c,{logo:s,alt:h,imageClassName:d}),null!=n&&(0,l.jsx)("b",{className:u,children:n})]})}},41463:(e,t,n)=>{"use strict";n.d(t,{A:()=>a});n(96540);var o=n(5260),r=n(74848);function a(e){let{locale:t,version:n,tag:a}=e;const i=t;return(0,r.jsxs)(o.A,{children:[t&&(0,r.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,r.jsx)("meta",{name:"docusaurus_version",content:n}),a&&(0,r.jsx)("meta",{name:"docusaurus_tag",content:a}),i&&(0,r.jsx)("meta",{name:"docsearch:language",content:i}),n&&(0,r.jsx)("meta",{name:"docsearch:version",content:n}),a&&(0,r.jsx)("meta",{name:"docsearch:docusaurus_tag",content:a})]})}},21122:(e,t,n)=>{"use strict";n.d(t,{A:()=>d});var o=n(96540),r=n(34164),a=n(92303),i=n(95293);const s={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var l=n(74848);function c(e){let{className:t,children:n}=e;const c=(0,a.A)(),{colorMode:d}=(0,i.G)();return(0,l.jsx)(l.Fragment,{children:(c?"dark"===d?["dark"]:["light"]:["light","dark"]).map((e=>{const a=n({theme:e,className:(0,r.A)(t,s.themedComponent,s[`themedComponent--${e}`])});return(0,l.jsx)(o.Fragment,{children:a},e)}))})}function d(e){const{sources:t,className:n,alt:o,...r}=e;return(0,l.jsx)(c,{className:n,children:e=>{let{theme:n,className:a}=e;return(0,l.jsx)("img",{src:t[n],alt:o,className:a,...r})}})}},41422:(e,t,n)=>{"use strict";n.d(t,{N:()=>g,u:()=>c});var o=n(96540),r=n(38193),a=n(205),i=n(53109),s=n(74848);const l="ease-in-out";function c(e){let{initialState:t}=e;const[n,r]=(0,o.useState)(t??!1),a=(0,o.useCallback)((()=>{r((e=>!e))}),[]);return{collapsed:n,setCollapsed:r,toggleCollapsed:a}}const d={display:"none",overflow:"hidden",height:"0px"},u={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?d:u;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function m(e){let{collapsibleRef:t,collapsed:n,animation:r}=e;const a=(0,o.useRef)(!1);(0,o.useEffect)((()=>{const e=t.current;function o(){const t=e.scrollHeight,n=r?.duration??function(e){if((0,i.O)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${r?.easing??l}`,height:`${t}px`}}function s(){const t=o();e.style.transition=t.transition,e.style.height=t.height}if(!a.current)return p(e,n),void(a.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(s(),requestAnimationFrame((()=>{e.style.height=d.height,e.style.overflow=d.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{s()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,r])}function f(e){if(!r.A.canUseDOM)return e?d:u}function h(e){let{as:t="div",collapsed:n,children:r,animation:a,onCollapseTransitionEnd:i,className:l,disableSSRStyle:c}=e;const d=(0,o.useRef)(null);return m({collapsibleRef:d,collapsed:n,animation:a}),(0,s.jsx)(t,{ref:d,style:c?void 0:f(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(d.current,n),i?.(n))},className:l,children:r})}function b(e){let{collapsed:t,...n}=e;const[r,i]=(0,o.useState)(!t),[l,c]=(0,o.useState)(t);return(0,a.A)((()=>{t||i(!0)}),[t]),(0,a.A)((()=>{r&&c(t)}),[r,t]),r?(0,s.jsx)(h,{...n,collapsed:l}):null}function g(e){let{lazy:t,...n}=e;const o=t?b:h;return(0,s.jsx)(o,{...n})}},65041:(e,t,n)=>{"use strict";n.d(t,{Mj:()=>h,oq:()=>f});var o=n(96540),r=n(92303),a=n(89466),i=n(89532),s=n(6342),l=n(74848);const c=(0,a.Wf)("docusaurus.announcement.dismiss"),d=(0,a.Wf)("docusaurus.announcement.id"),u=()=>"true"===c.get(),p=e=>c.set(String(e)),m=o.createContext(null);function f(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,s.p)(),t=(0,r.A)(),[n,a]=(0,o.useState)((()=>!!t&&u()));(0,o.useEffect)((()=>{a(u())}),[]);const i=(0,o.useCallback)((()=>{p(!0),a(!0)}),[]);return(0,o.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=d.get();"annoucement-bar"===n&&(n="announcement-bar");const o=t!==n;d.set(t),o&&p(!1),!o&&u()||a(!1)}),[e]),(0,o.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return(0,l.jsx)(m.Provider,{value:n,children:t})}function h(){const e=(0,o.useContext)(m);if(!e)throw new i.dV("AnnouncementBarProvider");return e}},95293:(e,t,n)=>{"use strict";n.d(t,{G:()=>g,a:()=>b});var o=n(96540),r=n(38193),a=n(89532),i=n(89466),s=n(6342),l=n(74848);const c=o.createContext(void 0),d="theme",u=(0,i.Wf)(d),p={light:"light",dark:"dark"},m=e=>e===p.dark?p.dark:p.light,f=e=>r.A.canUseDOM?m(document.documentElement.getAttribute("data-theme")):m(e),h=e=>{u.set(m(e))};function b(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,s.p)(),[r,a]=(0,o.useState)(f(e));(0,o.useEffect)((()=>{t&&u.del()}),[t]);const i=(0,o.useCallback)((function(t,o){void 0===o&&(o={});const{persist:r=!0}=o;t?(a(t),r&&h(t)):(a(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),u.del())}),[n,e]);(0,o.useEffect)((()=>{document.documentElement.setAttribute("data-theme",m(r))}),[r]),(0,o.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==d)return;const t=u.get();null!==t&&i(m(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,i]);const l=(0,o.useRef)(!1);return(0,o.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),o=()=>{window.matchMedia("print").matches||l.current?l.current=window.matchMedia("print").matches:i(null)};return e.addListener(o),()=>e.removeListener(o)}),[i,t,n]),(0,o.useMemo)((()=>({colorMode:r,setColorMode:i,get isDarkTheme(){return r===p.dark},setLightTheme(){i(p.light)},setDarkTheme(){i(p.dark)}})),[r,i])}();return(0,l.jsx)(c.Provider,{value:n,children:t})}function g(){const e=(0,o.useContext)(c);if(null==e)throw new a.dV("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},55597:(e,t,n)=>{"use strict";n.d(t,{VQ:()=>g,XK:()=>y,g1:()=>x});var o=n(96540),r=n(44070),a=n(17065),i=n(6342),s=n(84142),l=n(89532),c=n(89466),d=n(74848);const u=e=>`docs-preferred-version-${e}`,p={save:(e,t,n)=>{(0,c.Wf)(u(e),{persistence:t}).set(n)},read:(e,t)=>(0,c.Wf)(u(e),{persistence:t}).get(),clear:(e,t)=>{(0,c.Wf)(u(e),{persistence:t}).del()}},m=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const f=o.createContext(null);function h(){const e=(0,r.Gy)(),t=(0,i.p)().docs.versionPersistence,n=(0,o.useMemo)((()=>Object.keys(e)),[e]),[a,s]=(0,o.useState)((()=>m(n)));(0,o.useEffect)((()=>{s(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:o}=e;function r(e){const t=p.read(e,n);return o[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(p.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,r(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[a,(0,o.useMemo)((()=>({savePreferredVersion:function(e,n){p.save(e,t,n),s((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function b(e){let{children:t}=e;const n=h();return(0,d.jsx)(f.Provider,{value:n,children:t})}function g(e){let{children:t}=e;return s.C5?(0,d.jsx)(b,{children:t}):(0,d.jsx)(d.Fragment,{children:t})}function v(){const e=(0,o.useContext)(f);if(!e)throw new l.dV("DocsPreferredVersionContextProvider");return e}function x(e){void 0===e&&(e=a.W);const t=(0,r.ht)(e),[n,i]=v(),{preferredVersionName:s}=n[e];return{preferredVersion:t.versions.find((e=>e.name===s))??null,savePreferredVersionName:(0,o.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}function y(){const e=(0,r.Gy)(),[t]=v();function n(n){const o=e[n],{preferredVersionName:r}=t[n];return o.versions.find((e=>e.name===r))??null}const o=Object.keys(e);return Object.fromEntries(o.map((e=>[e,n(e)])))}},26588:(e,t,n)=>{"use strict";n.d(t,{V:()=>l,t:()=>c});var o=n(96540),r=n(89532),a=n(74848);const i=Symbol("EmptyContext"),s=o.createContext(i);function l(e){let{children:t,name:n,items:r}=e;const i=(0,o.useMemo)((()=>n&&r?{name:n,items:r}:null),[n,r]);return(0,a.jsx)(s.Provider,{value:i,children:t})}function c(){const e=(0,o.useContext)(s);if(e===i)throw new r.dV("DocsSidebarProvider");return e}},32252:(e,t,n)=>{"use strict";n.d(t,{n:()=>s,r:()=>l});var o=n(96540),r=n(89532),a=n(74848);const i=o.createContext(null);function s(e){let{children:t,version:n}=e;return(0,a.jsx)(i.Provider,{value:n,children:t})}function l(){const e=(0,o.useContext)(i);if(null===e)throw new r.dV("DocsVersionProvider");return e}},22069:(e,t,n)=>{"use strict";n.d(t,{M:()=>m,e:()=>p});var o=n(96540),r=n(75600),a=n(24581),i=n(57485),s=n(6342),l=n(89532),c=n(74848);const d=o.createContext(void 0);function u(){const e=function(){const e=(0,r.YL)(),{items:t}=(0,s.p)().navbar;return 0===t.length&&!e.component}(),t=(0,a.l)(),n=!e&&"mobile"===t,[l,c]=(0,o.useState)(!1);(0,i.$Z)((()=>{if(l)return c(!1),!1}));const d=(0,o.useCallback)((()=>{c((e=>!e))}),[]);return(0,o.useEffect)((()=>{"desktop"===t&&c(!1)}),[t]),(0,o.useMemo)((()=>({disabled:e,shouldRender:n,toggle:d,shown:l})),[e,n,d,l])}function p(e){let{children:t}=e;const n=u();return(0,c.jsx)(d.Provider,{value:n,children:t})}function m(){const e=o.useContext(d);if(void 0===e)throw new l.dV("NavbarMobileSidebarProvider");return e}},75600:(e,t,n)=>{"use strict";n.d(t,{GX:()=>c,YL:()=>l,y_:()=>s});var o=n(96540),r=n(89532),a=n(74848);const i=o.createContext(null);function s(e){let{children:t}=e;const n=(0,o.useState)({component:null,props:null});return(0,a.jsx)(i.Provider,{value:n,children:t})}function l(){const e=(0,o.useContext)(i);if(!e)throw new r.dV("NavbarSecondaryMenuContentProvider");return e[0]}function c(e){let{component:t,props:n}=e;const a=(0,o.useContext)(i);if(!a)throw new r.dV("NavbarSecondaryMenuContentProvider");const[,s]=a,l=(0,r.Be)(n);return(0,o.useEffect)((()=>{s({component:t,props:l})}),[s,t,l]),(0,o.useEffect)((()=>()=>s({component:null,props:null})),[s]),null}},14090:(e,t,n)=>{"use strict";n.d(t,{w:()=>r,J:()=>a});var o=n(96540);const r="navigation-with-keyboard";function a(){(0,o.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(r),"mousedown"===e.type&&document.body.classList.remove(r)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(r),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},24255:(e,t,n)=>{"use strict";n.d(t,{b:()=>s,w:()=>l});var o=n(96540),r=n(44586),a=n(57485);const i="q";function s(){return(0,a.l)(i)}function l(){const{siteConfig:{baseUrl:e,themeConfig:t}}=(0,r.A)(),{algolia:{searchPagePath:n}}=t;return(0,o.useCallback)((t=>`${e}${n}?${i}=${encodeURIComponent(t)}`),[e,n])}},24581:(e,t,n)=>{"use strict";n.d(t,{l:()=>s});var o=n(96540),r=n(38193);const a={desktop:"desktop",mobile:"mobile",ssr:"ssr"},i=996;function s(e){let{desktopBreakpoint:t=i}=void 0===e?{}:e;const[n,s]=(0,o.useState)((()=>"ssr"));return(0,o.useEffect)((()=>{function e(){s(function(e){if(!r.A.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?a.desktop:a.mobile}(t))}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[t]),n}},17559:(e,t,n)=>{"use strict";n.d(t,{G:()=>o});const o={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{}}},53109:(e,t,n)=>{"use strict";function o(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{O:()=>o})},84142:(e,t,n)=>{"use strict";n.d(t,{B5:()=>_,C5:()=>p,Nr:()=>m,OF:()=>y,QB:()=>S,Vd:()=>w,Y:()=>v,fW:()=>k,w8:()=>b});var o=n(96540),r=n(56347),a=n(22831),i=n(44070),s=n(55597),l=n(32252),c=n(26588),d=n(31682),u=n(99169);const p=!!i.Gy;function m(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=m(t);if(e)return e}}(e):void 0:e.href}const f=(e,t)=>void 0!==e&&(0,u.ys)(e,t),h=(e,t)=>e.some((e=>b(e,t)));function b(e,t){return"link"===e.type?f(e.href,t):"category"===e.type&&(f(e.href,t)||h(e.items,t))}function g(e,t){switch(e.type){case"category":return b(e,t)||e.items.some((e=>g(e,t)));case"link":return!e.unlisted||b(e,t);default:return!0}}function v(e,t){return(0,o.useMemo)((()=>e.filter((e=>g(e,t)))),[e,t])}function x(e){let{sidebarItems:t,pathname:n,onlyCategories:o=!1}=e;const r=[];return function e(t){for(const a of t)if("category"===a.type&&((0,u.ys)(a.href,n)||e(a.items))||"link"===a.type&&(0,u.ys)(a.href,n)){return o&&"category"!==a.type||r.unshift(a),!0}return!1}(t),r}function y(){const e=(0,c.t)(),{pathname:t}=(0,r.zy)(),n=(0,i.vT)()?.pluginData.breadcrumbs;return!1!==n&&e?x({sidebarItems:e.items,pathname:t}):null}function w(e){const{activeVersion:t}=(0,i.zK)(e),{preferredVersion:n}=(0,s.g1)(e),r=(0,i.r7)(e);return(0,o.useMemo)((()=>(0,d.s)([t,n,r].filter(Boolean))),[t,n,r])}function k(e,t){const n=w(t);return(0,o.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),o=t.find((t=>t[0]===e));if(!o)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return o[1]}),[e,n])}function S(e,t){const n=w(t);return(0,o.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),o=t.find((t=>t.id===e));if(!o){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${(0,d.s)(t.map((e=>e.id))).join("\n- ")}`)}return o}),[e,n])}function _(e){let{route:t}=e;const n=(0,r.zy)(),o=(0,l.r)(),i=t.routes,s=i.find((e=>(0,r.B6)(n.pathname,e)));if(!s)return null;const c=s.sidebar,d=c?o.docsSidebars[c]:void 0;return{docElement:(0,a.v)(i),sidebarName:c,sidebarItems:d}}},20481:(e,t,n)=>{"use strict";n.d(t,{s:()=>r});var o=n(44586);function r(e){const{siteConfig:t}=(0,o.A)(),{title:n,titleDelimiter:r}=t;return e?.trim().length?`${e.trim()} ${r} ${n}`:n}},57485:(e,t,n)=>{"use strict";n.d(t,{$Z:()=>i,aZ:()=>s,l:()=>l});var o=n(96540),r=n(56347),a=n(89532);function i(e){!function(e){const t=(0,r.W6)(),n=(0,a._q)(e);(0,o.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}function s(e){return function(e){const t=(0,r.W6)();return(0,o.useSyncExternalStore)(t.listen,(()=>e(t)),(()=>e(t)))}((t=>null===e?null:new URLSearchParams(t.location.search).get(e)))}function l(e){const t=s(e)??"",n=function(){const e=(0,r.W6)();return(0,o.useCallback)(((t,n,o)=>{const r=new URLSearchParams(e.location.search);n?r.set(t,n):r.delete(t),(o?.push?e.push:e.replace)({search:r.toString()})}),[e])}();return[t,(0,o.useCallback)(((t,o)=>{n(e,t,o)}),[n,e])]}},31682:(e,t,n)=>{"use strict";function o(e,t){return void 0===t&&(t=(e,t)=>e===t),e.filter(((n,o)=>e.findIndex((e=>t(e,n)))!==o))}function r(e){return Array.from(new Set(e))}n.d(t,{X:()=>o,s:()=>r})},69024:(e,t,n)=>{"use strict";n.d(t,{e3:()=>m,be:()=>u,Jx:()=>f});var o=n(96540),r=n(34164),a=n(5260),i=n(53102);function s(){const e=o.useContext(i.o);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var l=n(86025),c=n(20481),d=n(74848);function u(e){let{title:t,description:n,keywords:o,image:r,children:i}=e;const s=(0,c.s)(t),{withBaseUrl:u}=(0,l.h)(),p=r?u(r,{absolute:!0}):void 0;return(0,d.jsxs)(a.A,{children:[t&&(0,d.jsx)("title",{children:s}),t&&(0,d.jsx)("meta",{property:"og:title",content:s}),n&&(0,d.jsx)("meta",{name:"description",content:n}),n&&(0,d.jsx)("meta",{property:"og:description",content:n}),o&&(0,d.jsx)("meta",{name:"keywords",content:Array.isArray(o)?o.join(","):o}),p&&(0,d.jsx)("meta",{property:"og:image",content:p}),p&&(0,d.jsx)("meta",{name:"twitter:image",content:p}),i]})}const p=o.createContext(void 0);function m(e){let{className:t,children:n}=e;const i=o.useContext(p),s=(0,r.A)(i,t);return(0,d.jsxs)(p.Provider,{value:s,children:[(0,d.jsx)(a.A,{children:(0,d.jsx)("html",{className:s})}),n]})}function f(e){let{children:t}=e;const n=s(),o=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const a=`plugin-id-${n.plugin.id}`;return(0,d.jsx)(m,{className:(0,r.A)(o,a),children:t})}},89532:(e,t,n)=>{"use strict";n.d(t,{Be:()=>c,ZC:()=>s,_q:()=>i,dV:()=>l,fM:()=>d});var o=n(96540),r=n(205),a=n(74848);function i(e){const t=(0,o.useRef)(e);return(0,r.A)((()=>{t.current=e}),[e]),(0,o.useCallback)((function(){return t.current(...arguments)}),[])}function s(e){const t=(0,o.useRef)();return(0,r.A)((()=>{t.current=e})),t.current}class l extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function c(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,o.useMemo)((()=>e),t.flat())}function d(e){return t=>{let{children:n}=t;return(0,a.jsx)(a.Fragment,{children:e.reduceRight(((e,t)=>(0,a.jsx)(t,{children:e})),n)})}}},91252:(e,t,n)=>{"use strict";function o(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}n.d(t,{G:()=>o})},99169:(e,t,n)=>{"use strict";n.d(t,{Dt:()=>s,ys:()=>i});var o=n(96540),r=n(35947),a=n(44586);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function s(){const{baseUrl:e}=(0,a.A)().siteConfig;return(0,o.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function o(e){return e.path===t&&!0===e.exact}function r(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(o)||e(t.filter(r).flatMap((e=>e.routes??[])))}(n)}({routes:r.A,baseUrl:e})),[e])}},23104:(e,t,n)=>{"use strict";n.d(t,{Mq:()=>m,Tv:()=>d,a_:()=>f,gk:()=>h});var o=n(96540),r=n(38193),a=n(92303),i=n(205),s=n(89532),l=n(74848);const c=o.createContext(void 0);function d(e){let{children:t}=e;const n=function(){const e=(0,o.useRef)(!0);return(0,o.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,l.jsx)(c.Provider,{value:n,children:t})}function u(){const e=(0,o.useContext)(c);if(null==e)throw new s.dV("ScrollControllerProvider");return e}const p=()=>r.A.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function m(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=u(),r=(0,o.useRef)(p()),a=(0,s._q)(e);(0,o.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=p();a(e,r.current),r.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[a,n,...t])}function f(){const e=u(),t=function(){const e=(0,o.useRef)({elem:null,top:0}),t=(0,o.useCallback)((t=>{e.current={elem:t,top:t.getBoundingClientRect().top}}),[]),n=(0,o.useCallback)((()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const o=t.getBoundingClientRect().top-n;return o&&window.scrollBy({left:0,top:o}),e.current={elem:null,top:0},{restored:0!==o}}),[]);return(0,o.useMemo)((()=>({save:t,restore:n})),[n,t])}(),n=(0,o.useRef)(void 0),r=(0,o.useCallback)((o=>{t.save(o),e.disableScrollEvents(),n.current=()=>{const{restored:o}=t.restore();if(n.current=void 0,o){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}}),[e,t]);return(0,i.A)((()=>{queueMicrotask((()=>n.current?.()))})),{blockElementScrollPositionUntilNextRender:r}}function h(){const e=(0,o.useRef)(null),t=(0,a.A)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function o(){const r=document.documentElement.scrollTop;(n&&r>e||!n&&rt&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},2967:(e,t,n)=>{"use strict";n.d(t,{Cy:()=>i,af:()=>l,tU:()=>s});var o=n(44070),r=n(44586),a=n(55597);const i="default";function s(e,t){return`docs-${e}-${t}`}function l(){const{i18n:e}=(0,r.A)(),t=(0,o.Gy)(),n=(0,o.gk)(),l=(0,a.XK)();const c=[i,...Object.keys(t).map((function(e){const o=n?.activePlugin.pluginId===e?n.activeVersion:void 0,r=l[e],a=t[e].versions.find((e=>e.isLast));return s(e,(o??r??a).name)}))];return{locale:e.currentLocale,tags:c}}},89466:(e,t,n)=>{"use strict";n.d(t,{Dv:()=>d,Wf:()=>c});var o=n(96540);const r="localStorage";function a(e){let{key:t,oldValue:n,newValue:o,storage:r}=e;if(n===o)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,t,n,o,window.location.href,r),window.dispatchEvent(a)}function i(e){if(void 0===e&&(e=r),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,s||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),s=!0),null}var t}let s=!1;const l={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function c(e,t){if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(e);const n=i(t?.persistence);return null===n?l:{get:()=>{try{return n.getItem(e)}catch(t){return console.error(`Docusaurus storage error, can't get key=${e}`,t),null}},set:t=>{try{const o=n.getItem(e);n.setItem(e,t),a({key:e,oldValue:o,newValue:t,storage:n})}catch(o){console.error(`Docusaurus storage error, can't set ${e}=${t}`,o)}},del:()=>{try{const t=n.getItem(e);n.removeItem(e),a({key:e,oldValue:t,newValue:null,storage:n})}catch(t){console.error(`Docusaurus storage error, can't delete key=${e}`,t)}},listen:t=>{try{const o=o=>{o.storageArea===n&&o.key===e&&t(o)};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)}catch(o){return console.error(`Docusaurus storage error, can't listen for changes of key=${e}`,o),()=>{}}}}}function d(e,t){const n=(0,o.useRef)((()=>null===e?l:c(e,t))).current(),r=(0,o.useCallback)((e=>"undefined"==typeof window?()=>{}:n.listen(e)),[n]);return[(0,o.useSyncExternalStore)(r,(()=>"undefined"==typeof window?null:n.get()),(()=>null)),n]}},32131:(e,t,n)=>{"use strict";n.d(t,{o:()=>i});var o=n(44586),r=n(56347),a=n(70440);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:s}}=(0,o.A)(),{pathname:l}=(0,r.zy)(),c=(0,a.applyTrailingSlash)(l,{trailingSlash:n,baseUrl:e}),d=s===i?e:e.replace(`/${s}/`,"/"),u=c.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:o}=e;return`${o?t:""}${function(e){return e===i?`${d}`:`${d}${e}/`}(n)}${u}`}}}},75062:(e,t,n)=>{"use strict";n.d(t,{$:()=>i});var o=n(96540),r=n(56347),a=n(89532);function i(e){const t=(0,r.zy)(),n=(0,a.ZC)(t),i=(0,a._q)(e);(0,o.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6342:(e,t,n)=>{"use strict";n.d(t,{p:()=>r});var o=n(44586);function r(){return(0,o.A)().siteConfig.themeConfig}},38126:(e,t,n)=>{"use strict";n.d(t,{c:()=>r});var o=n(44586);function r(){const{siteConfig:{themeConfig:e}}=(0,o.A)();return e}},51062:(e,t,n)=>{"use strict";n.d(t,{C:()=>s});var o=n(96540),r=n(91252),a=n(86025),i=n(38126);function s(){const{withBaseUrl:e}=(0,a.h)(),{algolia:{externalUrlRegex:t,replaceSearchResultPathname:n}}=(0,i.c)();return(0,o.useCallback)((o=>{const a=new URL(o);if((0,r.G)(t,a.href))return o;const i=`${a.pathname+a.hash}`;return e(function(e,t){return t?e.replaceAll(new RegExp(t.from,"g"),t.to):e}(i,n))}),[e,t,n])}},12983:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){const{trailingSlash:n,baseUrl:o}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[r]=e.split(/[#?]/),a="/"===r||r===o?r:(i=r,n?function(e){return e.endsWith("/")?e:`${e}/`}(i):function(e){return e.endsWith("/")?e.slice(0,-1):e}(i));var i;return e.replace(r,a)}},80253:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=void 0,t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},70440:function(e,t,n){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="__blog-post-container";var r=n(12983);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return o(r).default}});var a=n(80253);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return a.getErrorCausalChain}})},31513:(e,t,n)=>{"use strict";n.d(t,{zR:()=>y,TM:()=>C,yJ:()=>m,sC:()=>A,AO:()=>p});var o=n(58168);function r(e){return"/"===e.charAt(0)}function a(e,t){for(var n=t,o=n+1,r=e.length;o=0;p--){var m=i[p];"."===m?a(i,p):".."===m?(a(i,p),u++):u&&(a(i,p),u--)}if(!c)for(;u--;u)i.unshift("..");!c||""===i[0]||i[0]&&r(i[0])||i.unshift("");var f=i.join("/");return n&&"/"!==f.substr(-1)&&(f+="/"),f};var s=n(11561);function l(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function d(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function u(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,o=e.hash,r=t||"/";return n&&"?"!==n&&(r+="?"===n.charAt(0)?n:"?"+n),o&&"#"!==o&&(r+="#"===o.charAt(0)?o:"#"+o),r}function m(e,t,n,r){var a;"string"==typeof e?(a=function(e){var t=e||"/",n="",o="",r=t.indexOf("#");-1!==r&&(o=t.substr(r),t=t.substr(0,r));var a=t.indexOf("?");return-1!==a&&(n=t.substr(a),t=t.substr(0,a)),{pathname:t,search:"?"===n?"":n,hash:"#"===o?"":o}}(e),a.state=t):(void 0===(a=(0,o.A)({},e)).pathname&&(a.pathname=""),a.search?"?"!==a.search.charAt(0)&&(a.search="?"+a.search):a.search="",a.hash?"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash):a.hash="",void 0!==t&&void 0===a.state&&(a.state=t));try{a.pathname=decodeURI(a.pathname)}catch(s){throw s instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):s}return n&&(a.key=n),r?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=i(a.pathname,r.pathname)):a.pathname=r.pathname:a.pathname||(a.pathname="/"),a}function f(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,o,r){if(null!=e){var a="function"==typeof e?e(t,n):e;"string"==typeof a?"function"==typeof o?o(a,r):r(!0):r(!1!==a)}else r(!0)},appendListener:function(e){var n=!0;function o(){n&&e.apply(void 0,arguments)}return t.push(o),function(){n=!1,t=t.filter((function(e){return e!==o}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),o=0;ot?n.splice(t,n.length-t,r):n.push(r),u({action:o,location:r,index:t,entries:n})}}))},replace:function(e,t){var o="REPLACE",r=m(e,t,h(),y.location);d.confirmTransitionTo(r,o,n,(function(e){e&&(y.entries[y.index]=r,u({action:o,location:r}))}))},go:x,goBack:function(){x(-1)},goForward:function(){x(1)},canGo:function(e){var t=y.index+e;return t>=0&&t{"use strict";var o=n(44363),r={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},s={};function l(e){return o.isMemo(e)?i:s[e.$$typeof]||r}s[o.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s[o.Memo]=i;var c=Object.defineProperty,d=Object.getOwnPropertyNames,u=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,m=Object.getPrototypeOf,f=Object.prototype;e.exports=function e(t,n,o){if("string"!=typeof n){if(f){var r=m(n);r&&r!==f&&e(t,r,o)}var i=d(n);u&&(i=i.concat(u(n)));for(var s=l(t),h=l(n),b=0;b{"use strict";e.exports=function(e,t,n,o,r,a,i,s){if(!e){var l;if(void 0===t)l=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,o,r,a,i,s],d=0;(l=new Error(t.replace(/%s/g,(function(){return c[d++]})))).name="Invariant Violation"}throw l.framesToPop=1,l}}},64634:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},89888:(e,t,n)=>{"use strict";n.r(t)},10119:(e,t,n)=>{"use strict";n.r(t)},5947:function(e,t,n){var o,r;o=function(){var e,t,n={version:"0.2.0"},o=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
'};function r(e,t,n){return en?n:e}function a(e){return 100*(-1+e)}function i(e,t,n){var r;return(r="translate3d"===o.positionUsing?{transform:"translate3d("+a(e)+"%,0,0)"}:"translate"===o.positionUsing?{transform:"translate("+a(e)+"%,0)"}:{"margin-left":a(e)+"%"}).transition="all "+t+"ms "+n,r}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(o[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=r(e,o.minimum,1),n.status=1===e?null:e;var a=n.render(!t),c=a.querySelector(o.barSelector),d=o.speed,u=o.easing;return a.offsetWidth,s((function(t){""===o.positionUsing&&(o.positionUsing=n.getPositioningCSS()),l(c,i(e,d,u)),1===e?(l(a,{transition:"none",opacity:1}),a.offsetWidth,setTimeout((function(){l(a,{transition:"all "+d+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),d)}),d)):setTimeout(t,d)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),o.trickleSpeed)};return o.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*r(Math.random()*t,.1,.95)),t=r(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*o.trickleRate)},e=0,t=0,n.promise=function(o){return o&&"resolved"!==o.state()?(0===t&&n.start(),e++,t++,o.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");d(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=o.template;var r,i=t.querySelector(o.barSelector),s=e?"-100":a(n.status||0),c=document.querySelector(o.parent);return l(i,{transition:"all 0 linear",transform:"translate3d("+s+"%,0,0)"}),o.showSpinner||(r=t.querySelector(o.spinnerSelector))&&m(r),c!=document.body&&d(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){u(document.documentElement,"nprogress-busy"),u(document.querySelector(o.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&m(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var s=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),l=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function o(t){var n=document.body.style;if(t in n)return t;for(var o,r=e.length,a=t.charAt(0).toUpperCase()+t.slice(1);r--;)if((o=e[r]+a)in n)return o;return t}function r(e){return e=n(e),t[e]||(t[e]=o(e))}function a(e,t,n){t=r(t),e.style[t]=n}return function(e,t){var n,o,r=arguments;if(2==r.length)for(n in t)void 0!==(o=t[n])&&t.hasOwnProperty(n)&&a(e,n,o);else a(e,r[1],r[2])}}();function c(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function d(e,t){var n=p(e),o=n+t;c(n,t)||(e.className=o.substring(1))}function u(e,t){var n,o=p(e);c(e,t)&&(n=o.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function m(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(r="function"==typeof o?o.call(t,n,t,e):o)||(e.exports=r)},35302:(e,t,n)=>{var o=n(64634);e.exports=m,e.exports.parse=a,e.exports.compile=function(e,t){return s(a(e,t),t)},e.exports.tokensToFunction=s,e.exports.tokensToRegExp=p;var r=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function a(e,t){for(var n,o=[],a=0,i=0,s="",d=t&&t.delimiter||"/";null!=(n=r.exec(e));){var u=n[0],p=n[1],m=n.index;if(s+=e.slice(i,m),i=m+u.length,p)s+=p[1];else{var f=e[i],h=n[2],b=n[3],g=n[4],v=n[5],x=n[6],y=n[7];s&&(o.push(s),s="");var w=null!=h&&null!=f&&f!==h,k="+"===x||"*"===x,S="?"===x||"*"===x,_=n[2]||d,E=g||v;o.push({name:b||a++,prefix:h||"",delimiter:_,optional:S,repeat:k,partial:w,asterisk:!!y,pattern:E?c(E):y?".*":"[^"+l(_)+"]+?"})}}return i{e.exports&&(e.exports={core:{meta:{path:"components/prism-core.js",option:"mandatory"},core:"Core"},themes:{meta:{path:"themes/{id}.css",link:"index.html?theme={id}",exclusive:!0},prism:{title:"Default",option:"default"},"prism-dark":"Dark","prism-funky":"Funky","prism-okaidia":{title:"Okaidia",owner:"ocodia"},"prism-twilight":{title:"Twilight",owner:"remybach"},"prism-coy":{title:"Coy",owner:"tshedor"},"prism-solarizedlight":{title:"Solarized Light",owner:"hectormatos2011 "},"prism-tomorrow":{title:"Tomorrow Night",owner:"Rosey"}},languages:{meta:{path:"components/prism-{id}",noCSS:!0,examplesPath:"examples/prism-{id}",addCheckAll:!0},markup:{title:"Markup",alias:["html","xml","svg","mathml","ssml","atom","rss"],aliasTitles:{html:"HTML",xml:"XML",svg:"SVG",mathml:"MathML",ssml:"SSML",atom:"Atom",rss:"RSS"},option:"default"},css:{title:"CSS",option:"default",modify:"markup"},clike:{title:"C-like",option:"default"},javascript:{title:"JavaScript",require:"clike",modify:"markup",optional:"regex",alias:"js",option:"default"},abap:{title:"ABAP",owner:"dellagustin"},abnf:{title:"ABNF",owner:"RunDevelopment"},actionscript:{title:"ActionScript",require:"javascript",modify:"markup",owner:"Golmote"},ada:{title:"Ada",owner:"Lucretia"},agda:{title:"Agda",owner:"xy-ren"},al:{title:"AL",owner:"RunDevelopment"},antlr4:{title:"ANTLR4",alias:"g4",owner:"RunDevelopment"},apacheconf:{title:"Apache Configuration",owner:"GuiTeK"},apex:{title:"Apex",require:["clike","sql"],owner:"RunDevelopment"},apl:{title:"APL",owner:"ngn"},applescript:{title:"AppleScript",owner:"Golmote"},aql:{title:"AQL",owner:"RunDevelopment"},arduino:{title:"Arduino",require:"cpp",alias:"ino",owner:"dkern"},arff:{title:"ARFF",owner:"Golmote"},armasm:{title:"ARM Assembly",alias:"arm-asm",owner:"RunDevelopment"},arturo:{title:"Arturo",alias:"art",optional:["bash","css","javascript","markup","markdown","sql"],owner:"drkameleon"},asciidoc:{alias:"adoc",title:"AsciiDoc",owner:"Golmote"},aspnet:{title:"ASP.NET (C#)",require:["markup","csharp"],owner:"nauzilus"},asm6502:{title:"6502 Assembly",owner:"kzurawel"},asmatmel:{title:"Atmel AVR Assembly",owner:"cerkit"},autohotkey:{title:"AutoHotkey",owner:"aviaryan"},autoit:{title:"AutoIt",owner:"Golmote"},avisynth:{title:"AviSynth",alias:"avs",owner:"Zinfidel"},"avro-idl":{title:"Avro IDL",alias:"avdl",owner:"RunDevelopment"},awk:{title:"AWK",alias:"gawk",aliasTitles:{gawk:"GAWK"},owner:"RunDevelopment"},bash:{title:"Bash",alias:["sh","shell"],aliasTitles:{sh:"Shell",shell:"Shell"},owner:"zeitgeist87"},basic:{title:"BASIC",owner:"Golmote"},batch:{title:"Batch",owner:"Golmote"},bbcode:{title:"BBcode",alias:"shortcode",aliasTitles:{shortcode:"Shortcode"},owner:"RunDevelopment"},bbj:{title:"BBj",owner:"hyyan"},bicep:{title:"Bicep",owner:"johnnyreilly"},birb:{title:"Birb",require:"clike",owner:"Calamity210"},bison:{title:"Bison",require:"c",owner:"Golmote"},bnf:{title:"BNF",alias:"rbnf",aliasTitles:{rbnf:"RBNF"},owner:"RunDevelopment"},bqn:{title:"BQN",owner:"yewscion"},brainfuck:{title:"Brainfuck",owner:"Golmote"},brightscript:{title:"BrightScript",owner:"RunDevelopment"},bro:{title:"Bro",owner:"wayward710"},bsl:{title:"BSL (1C:Enterprise)",alias:"oscript",aliasTitles:{oscript:"OneScript"},owner:"Diversus23"},c:{title:"C",require:"clike",owner:"zeitgeist87"},csharp:{title:"C#",require:"clike",alias:["cs","dotnet"],owner:"mvalipour"},cpp:{title:"C++",require:"c",owner:"zeitgeist87"},cfscript:{title:"CFScript",require:"clike",alias:"cfc",owner:"mjclemente"},chaiscript:{title:"ChaiScript",require:["clike","cpp"],owner:"RunDevelopment"},cil:{title:"CIL",owner:"sbrl"},cilkc:{title:"Cilk/C",require:"c",alias:"cilk-c",owner:"OpenCilk"},cilkcpp:{title:"Cilk/C++",require:"cpp",alias:["cilk-cpp","cilk"],owner:"OpenCilk"},clojure:{title:"Clojure",owner:"troglotit"},cmake:{title:"CMake",owner:"mjrogozinski"},cobol:{title:"COBOL",owner:"RunDevelopment"},coffeescript:{title:"CoffeeScript",require:"javascript",alias:"coffee",owner:"R-osey"},concurnas:{title:"Concurnas",alias:"conc",owner:"jasontatton"},csp:{title:"Content-Security-Policy",owner:"ScottHelme"},cooklang:{title:"Cooklang",owner:"ahue"},coq:{title:"Coq",owner:"RunDevelopment"},crystal:{title:"Crystal",require:"ruby",owner:"MakeNowJust"},"css-extras":{title:"CSS Extras",require:"css",modify:"css",owner:"milesj"},csv:{title:"CSV",owner:"RunDevelopment"},cue:{title:"CUE",owner:"RunDevelopment"},cypher:{title:"Cypher",owner:"RunDevelopment"},d:{title:"D",require:"clike",owner:"Golmote"},dart:{title:"Dart",require:"clike",owner:"Golmote"},dataweave:{title:"DataWeave",owner:"machaval"},dax:{title:"DAX",owner:"peterbud"},dhall:{title:"Dhall",owner:"RunDevelopment"},diff:{title:"Diff",owner:"uranusjr"},django:{title:"Django/Jinja2",require:"markup-templating",alias:"jinja2",owner:"romanvm"},"dns-zone-file":{title:"DNS zone file",owner:"RunDevelopment",alias:"dns-zone"},docker:{title:"Docker",alias:"dockerfile",owner:"JustinBeckwith"},dot:{title:"DOT (Graphviz)",alias:"gv",optional:"markup",owner:"RunDevelopment"},ebnf:{title:"EBNF",owner:"RunDevelopment"},editorconfig:{title:"EditorConfig",owner:"osipxd"},eiffel:{title:"Eiffel",owner:"Conaclos"},ejs:{title:"EJS",require:["javascript","markup-templating"],owner:"RunDevelopment",alias:"eta",aliasTitles:{eta:"Eta"}},elixir:{title:"Elixir",owner:"Golmote"},elm:{title:"Elm",owner:"zwilias"},etlua:{title:"Embedded Lua templating",require:["lua","markup-templating"],owner:"RunDevelopment"},erb:{title:"ERB",require:["ruby","markup-templating"],owner:"Golmote"},erlang:{title:"Erlang",owner:"Golmote"},"excel-formula":{title:"Excel Formula",alias:["xlsx","xls"],owner:"RunDevelopment"},fsharp:{title:"F#",require:"clike",owner:"simonreynolds7"},factor:{title:"Factor",owner:"catb0t"},false:{title:"False",owner:"edukisto"},"firestore-security-rules":{title:"Firestore security rules",require:"clike",owner:"RunDevelopment"},flow:{title:"Flow",require:"javascript",owner:"Golmote"},fortran:{title:"Fortran",owner:"Golmote"},ftl:{title:"FreeMarker Template Language",require:"markup-templating",owner:"RunDevelopment"},gml:{title:"GameMaker Language",alias:"gamemakerlanguage",require:"clike",owner:"LiarOnce"},gap:{title:"GAP (CAS)",owner:"RunDevelopment"},gcode:{title:"G-code",owner:"RunDevelopment"},gdscript:{title:"GDScript",owner:"RunDevelopment"},gedcom:{title:"GEDCOM",owner:"Golmote"},gettext:{title:"gettext",alias:"po",owner:"RunDevelopment"},gherkin:{title:"Gherkin",owner:"hason"},git:{title:"Git",owner:"lgiraudel"},glsl:{title:"GLSL",require:"c",owner:"Golmote"},gn:{title:"GN",alias:"gni",owner:"RunDevelopment"},"linker-script":{title:"GNU Linker Script",alias:"ld",owner:"RunDevelopment"},go:{title:"Go",require:"clike",owner:"arnehormann"},"go-module":{title:"Go module",alias:"go-mod",owner:"RunDevelopment"},gradle:{title:"Gradle",require:"clike",owner:"zeabdelkhalek-badido18"},graphql:{title:"GraphQL",optional:"markdown",owner:"Golmote"},groovy:{title:"Groovy",require:"clike",owner:"robfletcher"},haml:{title:"Haml",require:"ruby",optional:["css","css-extras","coffeescript","erb","javascript","less","markdown","scss","textile"],owner:"Golmote"},handlebars:{title:"Handlebars",require:"markup-templating",alias:["hbs","mustache"],aliasTitles:{mustache:"Mustache"},owner:"Golmote"},haskell:{title:"Haskell",alias:"hs",owner:"bholst"},haxe:{title:"Haxe",require:"clike",optional:"regex",owner:"Golmote"},hcl:{title:"HCL",owner:"outsideris"},hlsl:{title:"HLSL",require:"c",owner:"RunDevelopment"},hoon:{title:"Hoon",owner:"matildepark"},http:{title:"HTTP",optional:["csp","css","hpkp","hsts","javascript","json","markup","uri"],owner:"danielgtaylor"},hpkp:{title:"HTTP Public-Key-Pins",owner:"ScottHelme"},hsts:{title:"HTTP Strict-Transport-Security",owner:"ScottHelme"},ichigojam:{title:"IchigoJam",owner:"BlueCocoa"},icon:{title:"Icon",owner:"Golmote"},"icu-message-format":{title:"ICU Message Format",owner:"RunDevelopment"},idris:{title:"Idris",alias:"idr",owner:"KeenS",require:"haskell"},ignore:{title:".ignore",owner:"osipxd",alias:["gitignore","hgignore","npmignore"],aliasTitles:{gitignore:".gitignore",hgignore:".hgignore",npmignore:".npmignore"}},inform7:{title:"Inform 7",owner:"Golmote"},ini:{title:"Ini",owner:"aviaryan"},io:{title:"Io",owner:"AlesTsurko"},j:{title:"J",owner:"Golmote"},java:{title:"Java",require:"clike",owner:"sherblot"},javadoc:{title:"JavaDoc",require:["markup","java","javadoclike"],modify:"java",optional:"scala",owner:"RunDevelopment"},javadoclike:{title:"JavaDoc-like",modify:["java","javascript","php"],owner:"RunDevelopment"},javastacktrace:{title:"Java stack trace",owner:"RunDevelopment"},jexl:{title:"Jexl",owner:"czosel"},jolie:{title:"Jolie",require:"clike",owner:"thesave"},jq:{title:"JQ",owner:"RunDevelopment"},jsdoc:{title:"JSDoc",require:["javascript","javadoclike","typescript"],modify:"javascript",optional:["actionscript","coffeescript"],owner:"RunDevelopment"},"js-extras":{title:"JS Extras",require:"javascript",modify:"javascript",optional:["actionscript","coffeescript","flow","n4js","typescript"],owner:"RunDevelopment"},json:{title:"JSON",alias:"webmanifest",aliasTitles:{webmanifest:"Web App Manifest"},owner:"CupOfTea696"},json5:{title:"JSON5",require:"json",owner:"RunDevelopment"},jsonp:{title:"JSONP",require:"json",owner:"RunDevelopment"},jsstacktrace:{title:"JS stack trace",owner:"sbrl"},"js-templates":{title:"JS Templates",require:"javascript",modify:"javascript",optional:["css","css-extras","graphql","markdown","markup","sql"],owner:"RunDevelopment"},julia:{title:"Julia",owner:"cdagnino"},keepalived:{title:"Keepalived Configure",owner:"dev-itsheng"},keyman:{title:"Keyman",owner:"mcdurdin"},kotlin:{title:"Kotlin",alias:["kt","kts"],aliasTitles:{kts:"Kotlin Script"},require:"clike",owner:"Golmote"},kumir:{title:"KuMir (\u041a\u0443\u041c\u0438\u0440)",alias:"kum",owner:"edukisto"},kusto:{title:"Kusto",owner:"RunDevelopment"},latex:{title:"LaTeX",alias:["tex","context"],aliasTitles:{tex:"TeX",context:"ConTeXt"},owner:"japborst"},latte:{title:"Latte",require:["clike","markup-templating","php"],owner:"nette"},less:{title:"Less",require:"css",optional:"css-extras",owner:"Golmote"},lilypond:{title:"LilyPond",require:"scheme",alias:"ly",owner:"RunDevelopment"},liquid:{title:"Liquid",require:"markup-templating",owner:"cinhtau"},lisp:{title:"Lisp",alias:["emacs","elisp","emacs-lisp"],owner:"JuanCaicedo"},livescript:{title:"LiveScript",owner:"Golmote"},llvm:{title:"LLVM IR",owner:"porglezomp"},log:{title:"Log file",optional:"javastacktrace",owner:"RunDevelopment"},lolcode:{title:"LOLCODE",owner:"Golmote"},lua:{title:"Lua",owner:"Golmote"},magma:{title:"Magma (CAS)",owner:"RunDevelopment"},makefile:{title:"Makefile",owner:"Golmote"},markdown:{title:"Markdown",require:"markup",optional:"yaml",alias:"md",owner:"Golmote"},"markup-templating":{title:"Markup templating",require:"markup",owner:"Golmote"},mata:{title:"Mata",owner:"RunDevelopment"},matlab:{title:"MATLAB",owner:"Golmote"},maxscript:{title:"MAXScript",owner:"RunDevelopment"},mel:{title:"MEL",owner:"Golmote"},mermaid:{title:"Mermaid",owner:"RunDevelopment"},metafont:{title:"METAFONT",owner:"LaeriExNihilo"},mizar:{title:"Mizar",owner:"Golmote"},mongodb:{title:"MongoDB",owner:"airs0urce",require:"javascript"},monkey:{title:"Monkey",owner:"Golmote"},moonscript:{title:"MoonScript",alias:"moon",owner:"RunDevelopment"},n1ql:{title:"N1QL",owner:"TMWilds"},n4js:{title:"N4JS",require:"javascript",optional:"jsdoc",alias:"n4jsd",owner:"bsmith-n4"},"nand2tetris-hdl":{title:"Nand To Tetris HDL",owner:"stephanmax"},naniscript:{title:"Naninovel Script",owner:"Elringus",alias:"nani"},nasm:{title:"NASM",owner:"rbmj"},neon:{title:"NEON",owner:"nette"},nevod:{title:"Nevod",owner:"nezaboodka"},nginx:{title:"nginx",owner:"volado"},nim:{title:"Nim",owner:"Golmote"},nix:{title:"Nix",owner:"Golmote"},nsis:{title:"NSIS",owner:"idleberg"},objectivec:{title:"Objective-C",require:"c",alias:"objc",owner:"uranusjr"},ocaml:{title:"OCaml",owner:"Golmote"},odin:{title:"Odin",owner:"edukisto"},opencl:{title:"OpenCL",require:"c",modify:["c","cpp"],owner:"Milania1"},openqasm:{title:"OpenQasm",alias:"qasm",owner:"RunDevelopment"},oz:{title:"Oz",owner:"Golmote"},parigp:{title:"PARI/GP",owner:"Golmote"},parser:{title:"Parser",require:"markup",owner:"Golmote"},pascal:{title:"Pascal",alias:"objectpascal",aliasTitles:{objectpascal:"Object Pascal"},owner:"Golmote"},pascaligo:{title:"Pascaligo",owner:"DefinitelyNotAGoat"},psl:{title:"PATROL Scripting Language",owner:"bertysentry"},pcaxis:{title:"PC-Axis",alias:"px",owner:"RunDevelopment"},peoplecode:{title:"PeopleCode",alias:"pcode",owner:"RunDevelopment"},perl:{title:"Perl",owner:"Golmote"},php:{title:"PHP",require:"markup-templating",owner:"milesj"},phpdoc:{title:"PHPDoc",require:["php","javadoclike"],modify:"php",owner:"RunDevelopment"},"php-extras":{title:"PHP Extras",require:"php",modify:"php",owner:"milesj"},"plant-uml":{title:"PlantUML",alias:"plantuml",owner:"RunDevelopment"},plsql:{title:"PL/SQL",require:"sql",owner:"Golmote"},powerquery:{title:"PowerQuery",alias:["pq","mscript"],owner:"peterbud"},powershell:{title:"PowerShell",owner:"nauzilus"},processing:{title:"Processing",require:"clike",owner:"Golmote"},prolog:{title:"Prolog",owner:"Golmote"},promql:{title:"PromQL",owner:"arendjr"},properties:{title:".properties",owner:"Golmote"},protobuf:{title:"Protocol Buffers",require:"clike",owner:"just-boris"},pug:{title:"Pug",require:["markup","javascript"],optional:["coffeescript","ejs","handlebars","less","livescript","markdown","scss","stylus","twig"],owner:"Golmote"},puppet:{title:"Puppet",owner:"Golmote"},pure:{title:"Pure",optional:["c","cpp","fortran"],owner:"Golmote"},purebasic:{title:"PureBasic",require:"clike",alias:"pbfasm",owner:"HeX0R101"},purescript:{title:"PureScript",require:"haskell",alias:"purs",owner:"sriharshachilakapati"},python:{title:"Python",alias:"py",owner:"multipetros"},qsharp:{title:"Q#",require:"clike",alias:"qs",owner:"fedonman"},q:{title:"Q (kdb+ database)",owner:"Golmote"},qml:{title:"QML",require:"javascript",owner:"RunDevelopment"},qore:{title:"Qore",require:"clike",owner:"temnroegg"},r:{title:"R",owner:"Golmote"},racket:{title:"Racket",require:"scheme",alias:"rkt",owner:"RunDevelopment"},cshtml:{title:"Razor C#",alias:"razor",require:["markup","csharp"],optional:["css","css-extras","javascript","js-extras"],owner:"RunDevelopment"},jsx:{title:"React JSX",require:["markup","javascript"],optional:["jsdoc","js-extras","js-templates"],owner:"vkbansal"},tsx:{title:"React TSX",require:["jsx","typescript"]},reason:{title:"Reason",require:"clike",owner:"Golmote"},regex:{title:"Regex",owner:"RunDevelopment"},rego:{title:"Rego",owner:"JordanSh"},renpy:{title:"Ren'py",alias:"rpy",owner:"HyuchiaDiego"},rescript:{title:"ReScript",alias:"res",owner:"vmarcosp"},rest:{title:"reST (reStructuredText)",owner:"Golmote"},rip:{title:"Rip",owner:"ravinggenius"},roboconf:{title:"Roboconf",owner:"Golmote"},robotframework:{title:"Robot Framework",alias:"robot",owner:"RunDevelopment"},ruby:{title:"Ruby",require:"clike",alias:"rb",owner:"samflores"},rust:{title:"Rust",owner:"Golmote"},sas:{title:"SAS",optional:["groovy","lua","sql"],owner:"Golmote"},sass:{title:"Sass (Sass)",require:"css",optional:"css-extras",owner:"Golmote"},scss:{title:"Sass (SCSS)",require:"css",optional:"css-extras",owner:"MoOx"},scala:{title:"Scala",require:"java",owner:"jozic"},scheme:{title:"Scheme",owner:"bacchus123"},"shell-session":{title:"Shell session",require:"bash",alias:["sh-session","shellsession"],owner:"RunDevelopment"},smali:{title:"Smali",owner:"RunDevelopment"},smalltalk:{title:"Smalltalk",owner:"Golmote"},smarty:{title:"Smarty",require:"markup-templating",optional:"php",owner:"Golmote"},sml:{title:"SML",alias:"smlnj",aliasTitles:{smlnj:"SML/NJ"},owner:"RunDevelopment"},solidity:{title:"Solidity (Ethereum)",alias:"sol",require:"clike",owner:"glachaud"},"solution-file":{title:"Solution file",alias:"sln",owner:"RunDevelopment"},soy:{title:"Soy (Closure Template)",require:"markup-templating",owner:"Golmote"},sparql:{title:"SPARQL",require:"turtle",owner:"Triply-Dev",alias:"rq"},"splunk-spl":{title:"Splunk SPL",owner:"RunDevelopment"},sqf:{title:"SQF: Status Quo Function (Arma 3)",require:"clike",owner:"RunDevelopment"},sql:{title:"SQL",owner:"multipetros"},squirrel:{title:"Squirrel",require:"clike",owner:"RunDevelopment"},stan:{title:"Stan",owner:"RunDevelopment"},stata:{title:"Stata Ado",require:["mata","java","python"],owner:"RunDevelopment"},iecst:{title:"Structured Text (IEC 61131-3)",owner:"serhioromano"},stylus:{title:"Stylus",owner:"vkbansal"},supercollider:{title:"SuperCollider",alias:"sclang",owner:"RunDevelopment"},swift:{title:"Swift",owner:"chrischares"},systemd:{title:"Systemd configuration file",owner:"RunDevelopment"},"t4-templating":{title:"T4 templating",owner:"RunDevelopment"},"t4-cs":{title:"T4 Text Templates (C#)",require:["t4-templating","csharp"],alias:"t4",owner:"RunDevelopment"},"t4-vb":{title:"T4 Text Templates (VB)",require:["t4-templating","vbnet"],owner:"RunDevelopment"},tap:{title:"TAP",owner:"isaacs",require:"yaml"},tcl:{title:"Tcl",owner:"PeterChaplin"},tt2:{title:"Template Toolkit 2",require:["clike","markup-templating"],owner:"gflohr"},textile:{title:"Textile",require:"markup",optional:"css",owner:"Golmote"},toml:{title:"TOML",owner:"RunDevelopment"},tremor:{title:"Tremor",alias:["trickle","troy"],owner:"darach",aliasTitles:{trickle:"trickle",troy:"troy"}},turtle:{title:"Turtle",alias:"trig",aliasTitles:{trig:"TriG"},owner:"jakubklimek"},twig:{title:"Twig",require:"markup-templating",owner:"brandonkelly"},typescript:{title:"TypeScript",require:"javascript",optional:"js-templates",alias:"ts",owner:"vkbansal"},typoscript:{title:"TypoScript",alias:"tsconfig",aliasTitles:{tsconfig:"TSConfig"},owner:"dkern"},unrealscript:{title:"UnrealScript",alias:["uscript","uc"],owner:"RunDevelopment"},uorazor:{title:"UO Razor Script",owner:"jaseowns"},uri:{title:"URI",alias:"url",aliasTitles:{url:"URL"},owner:"RunDevelopment"},v:{title:"V",require:"clike",owner:"taggon"},vala:{title:"Vala",require:"clike",optional:"regex",owner:"TemplarVolk"},vbnet:{title:"VB.Net",require:"basic",owner:"Bigsby"},velocity:{title:"Velocity",require:"markup",owner:"Golmote"},verilog:{title:"Verilog",owner:"a-rey"},vhdl:{title:"VHDL",owner:"a-rey"},vim:{title:"vim",owner:"westonganger"},"visual-basic":{title:"Visual Basic",alias:["vb","vba"],aliasTitles:{vba:"VBA"},owner:"Golmote"},warpscript:{title:"WarpScript",owner:"RunDevelopment"},wasm:{title:"WebAssembly",owner:"Golmote"},"web-idl":{title:"Web IDL",alias:"webidl",owner:"RunDevelopment"},wgsl:{title:"WGSL",owner:"Dr4gonthree"},wiki:{title:"Wiki markup",require:"markup",owner:"Golmote"},wolfram:{title:"Wolfram language",alias:["mathematica","nb","wl"],aliasTitles:{mathematica:"Mathematica",nb:"Mathematica Notebook"},owner:"msollami"},wren:{title:"Wren",owner:"clsource"},xeora:{title:"Xeora",require:"markup",alias:"xeoracube",aliasTitles:{xeoracube:"XeoraCube"},owner:"freakmaxi"},"xml-doc":{title:"XML doc (.net)",require:"markup",modify:["csharp","fsharp","vbnet"],owner:"RunDevelopment"},xojo:{title:"Xojo (REALbasic)",owner:"Golmote"},xquery:{title:"XQuery",require:"markup",owner:"Golmote"},yaml:{title:"YAML",alias:"yml",owner:"hason"},yang:{title:"YANG",owner:"RunDevelopment"},zig:{title:"Zig",owner:"RunDevelopment"}},plugins:{meta:{path:"plugins/{id}/prism-{id}",link:"plugins/{id}/"},"line-highlight":{title:"Line Highlight",description:"Highlights specific lines and/or line ranges."},"line-numbers":{title:"Line Numbers",description:"Line number at the beginning of code lines.",owner:"kuba-kubula"},"show-invisibles":{title:"Show Invisibles",description:"Show hidden characters such as tabs and line breaks.",optional:["autolinker","data-uri-highlight"]},autolinker:{title:"Autolinker",description:"Converts URLs and emails in code to clickable links. Parses Markdown links in comments."},wpd:{title:"WebPlatform Docs",description:'Makes tokens link to WebPlatform.org documentation. The links open in a new tab.'},"custom-class":{title:"Custom Class",description:"This plugin allows you to prefix Prism's default classes (.comment can become .namespace--comment) or replace them with your defined ones (like .editor__comment). You can even add new classes.",owner:"dvkndn",noCSS:!0},"file-highlight":{title:"File Highlight",description:"Fetch external files and highlight them with Prism. Used on the Prism website itself.",noCSS:!0},"show-language":{title:"Show Language",description:"Display the highlighted language in code blocks (inline code does not show the label).",owner:"nauzilus",noCSS:!0,require:"toolbar"},"jsonp-highlight":{title:"JSONP Highlight",description:"Fetch content with JSONP and highlight some interesting content (e.g. GitHub/Gists or Bitbucket API).",noCSS:!0,owner:"nauzilus"},"highlight-keywords":{title:"Highlight Keywords",description:"Adds special CSS classes for each keyword for fine-grained highlighting.",owner:"vkbansal",noCSS:!0},"remove-initial-line-feed":{title:"Remove initial line feed",description:"Removes the initial line feed in code blocks.",owner:"Golmote",noCSS:!0},"inline-color":{title:"Inline color",description:"Adds a small inline preview for colors in style sheets.",require:"css-extras",owner:"RunDevelopment"},previewers:{title:"Previewers",description:"Previewers for angles, colors, gradients, easing and time.",require:"css-extras",owner:"Golmote"},autoloader:{title:"Autoloader",description:"Automatically loads the needed languages to highlight the code blocks.",owner:"Golmote",noCSS:!0},"keep-markup":{title:"Keep Markup",description:"Prevents custom markup from being dropped out during highlighting.",owner:"Golmote",optional:"normalize-whitespace",noCSS:!0},"command-line":{title:"Command Line",description:"Display a command line with a prompt and, optionally, the output/response from the commands.",owner:"chriswells0"},"unescaped-markup":{title:"Unescaped Markup",description:"Write markup without having to escape anything."},"normalize-whitespace":{title:"Normalize Whitespace",description:"Supports multiple operations to normalize whitespace in code blocks.",owner:"zeitgeist87",optional:"unescaped-markup",noCSS:!0},"data-uri-highlight":{title:"Data-URI Highlight",description:"Highlights data-URI contents.",owner:"Golmote",noCSS:!0},toolbar:{title:"Toolbar",description:"Attach a toolbar for plugins to easily register buttons on the top of a code block.",owner:"mAAdhaTTah"},"copy-to-clipboard":{title:"Copy to Clipboard Button",description:"Add a button that copies the code block to the clipboard when clicked.",owner:"mAAdhaTTah",require:"toolbar",noCSS:!0},"download-button":{title:"Download Button",description:"A button in the toolbar of a code block adding a convenient way to download a code file.",owner:"Golmote",require:"toolbar",noCSS:!0},"match-braces":{title:"Match braces",description:"Highlights matching braces.",owner:"RunDevelopment"},"diff-highlight":{title:"Diff Highlight",description:"Highlights the code inside diff blocks.",owner:"RunDevelopment",require:"diff"},"filter-highlight-all":{title:"Filter highlightAll",description:"Filters the elements the highlightAll and highlightAllUnder methods actually highlight.",owner:"RunDevelopment",noCSS:!0},treeview:{title:"Treeview",description:"A language with special styles to highlight file system tree structures.",owner:"Golmote"}}})},8722:(e,t,n)=>{const o=n(6969),r=n(98380),a=new Set;function i(e){void 0===e?e=Object.keys(o.languages).filter((e=>"meta"!=e)):Array.isArray(e)||(e=[e]);const t=[...a,...Object.keys(Prism.languages)];r(o,e,t).load((e=>{if(!(e in o.languages))return void(i.silent||console.warn("Language does not exist: "+e));const t="./prism-"+e;delete n.c[n(63157).resolve(t)],delete Prism.languages[e],n(63157)(t),a.add(e)}))}i.silent=!1,e.exports=i},19700:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,o,r,a){if(n.language===o){var i=n.tokenStack=[];n.code=n.code.replace(r,(function(e){if("function"==typeof a&&!a(e))return e;for(var r,s=i.length;-1!==n.code.indexOf(r=t(o,s));)++s;return i[s]=e,r})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,o){if(n.language===o&&n.tokenStack){n.grammar=e.languages[o];var r=0,a=Object.keys(n.tokenStack);!function i(s){for(var l=0;l=a.length);l++){var c=s[l];if("string"==typeof c||c.content&&"string"==typeof c.content){var d=a[r],u=n.tokenStack[d],p="string"==typeof c?c:c.content,m=t(o,d),f=p.indexOf(m);if(f>-1){++r;var h=p.substring(0,f),b=new e.Token(o,e.tokenize(u,n.grammar),"language-"+o,u),g=p.substring(f+m.length),v=[];h&&v.push.apply(v,i([h])),v.push(b),g&&v.push.apply(v,i([g])),"string"==typeof c?s.splice.apply(s,[l,1].concat(v)):c.content=v}}else c.content&&i(c.content)}return s}(n.tokens)}}}})}(Prism)},18692:(e,t,n)=>{var o={"./":8722};function r(e){var t=a(e);return n(t)}function a(e){if(!n.o(o,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return o[e]}r.keys=function(){return Object.keys(o)},r.resolve=a,e.exports=r,r.id=18692},63157:(e,t,n)=>{var o={"./":8722};function r(e){var t=a(e);return n(t)}function a(e){if(!n.o(o,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return o[e]}r.keys=function(){return Object.keys(o)},r.resolve=a,e.exports=r,r.id=63157},98380:e=>{"use strict";var t=function(){var e=function(){};function t(e,t){Array.isArray(e)?e.forEach(t):null!=e&&t(e,0)}function n(e){for(var t={},n=0,o=e.length;n "));var s={},l=e[o];if(l){function c(t){if(!(t in e))throw new Error(o+" depends on an unknown component "+t);if(!(t in s))for(var i in r(t,a),s[t]=!0,n[t])s[i]=!0}t(l.require,c),t(l.optional,c),t(l.modify,c)}n[o]=s,a.pop()}}return function(e){var t=n[e];return t||(r(e,o),t=n[e]),t}}function r(e){for(var t in e)return!0;return!1}return function(a,i,s){var l=function(e){var t={};for(var n in e){var o=e[n];for(var r in o)if("meta"!=r){var a=o[r];t[r]="string"==typeof a?{title:a}:a}}return t}(a),c=function(e){var n;return function(o){if(o in e)return o;if(!n)for(var r in n={},e){var a=e[r];t(a&&a.alias,(function(t){if(t in n)throw new Error(t+" cannot be alias for both "+r+" and "+n[t]);if(t in e)throw new Error(t+" cannot be alias of "+r+" because it is a component.");n[t]=r}))}return n[o]||o}}(l);i=i.map(c),s=(s||[]).map(c);var d=n(i),u=n(s);i.forEach((function e(n){var o=l[n];t(o&&o.require,(function(t){t in u||(d[t]=!0,e(t))}))}));for(var p,m=o(l),f=d;r(f);){for(var h in p={},f){var b=l[h];t(b&&b.modify,(function(e){e in u&&(p[e]=!0)}))}for(var g in u)if(!(g in d))for(var v in m(g))if(v in d){p[g]=!0;break}for(var x in f=p)d[x]=!0}var y={getIds:function(){var e=[];return y.load((function(t){e.push(t)})),e},load:function(t,n){return function(t,n,o,r){var a=r?r.series:void 0,i=r?r.parallel:e,s={},l={};function c(e){if(e in s)return s[e];l[e]=!0;var r,d=[];for(var u in t(e))u in n&&d.push(u);if(0===d.length)r=o(e);else{var p=i(d.map((function(e){var t=c(e);return delete l[e],t})));a?r=a(p,(function(){return o(e)})):o(e)}return s[e]=r}for(var d in n)c(d);var u=[];for(var p in l)u.push(s[p]);return i(u)}(m,d,t,n)}};return y}}();e.exports=t},2694:(e,t,n)=>{"use strict";var o=n(6925);function r(){}function a(){}a.resetWarningCache=r,e.exports=function(){function e(e,t,n,r,a,i){if(i!==o){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:r};return n.PropTypes=n,n}},5556:(e,t,n)=>{e.exports=n(2694)()},6925:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},22551:(e,t,n)=>{"use strict";var o=n(96540),r=n(69982);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n