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/404.html b/404.html index 287df255ab..6de75da90a 100644 --- a/404.html +++ b/404.html @@ -16,8 +16,8 @@ - - + +
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.
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: Contextthis
",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: ContextPermission
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.withPermPermission
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.withPermMongoDBStore
",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 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 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: ContextAppController
",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: Contextdev
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 --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 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: RepositoryServiceManager
",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: Repositoryproduction
",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/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/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 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 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(): PromisegRPC
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(): PromiseAuthorization
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 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 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): PromiseYour Docusaurus site did not load properly.
\nA very common reason is a wrong site baseUrl configuration.
\nCurrent configured baseUrl = ${e} ${"/"===e?" (default value)":""}
\nWe suggest trying baseUrl =
\n