-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path024de627.5d32dea5.js
1 lines (1 loc) · 18.2 KB
/
024de627.5d32dea5.js
1
"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[7865],{197:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>l});const o=JSON.parse('{"id":"tutorial/Customization/hooks","title":"Hooks","description":"Hooks are powerful tools to modify the data before it is saved to the database, execute something after data were saved or deleted, change the query before fetching items from the database, modify the fetched data before it is displayed in the list and show, and prevent the request to db depending on some condition.","source":"@site/docs/tutorial/03-Customization/04-hooks.md","sourceDirName":"tutorial/03-Customization","slug":"/tutorial/Customization/hooks","permalink":"/docs/tutorial/Customization/hooks","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":4,"frontMatter":{"description":"Hooks are powerful tools to modify the data before it is saved to the database, execute something after data were saved or deleted, change the query before fetching items from the database, modify the fetched data before it is displayed in the list and show, and prevent the request to db depending on some condition.","image":"/ogs/hooks.png"},"sidebar":"tutorialSidebar","previous":{"title":"Virtual columns","permalink":"/docs/tutorial/Customization/virtualColumns"},"next":{"title":"Limiting actions access","permalink":"/docs/tutorial/Customization/limitingAccess"}}');var n=s(4848),i=s(8453);const a={description:"Hooks are powerful tools to modify the data before it is saved to the database, execute something after data were saved or deleted, change the query before fetching items from the database, modify the fetched data before it is displayed in the list and show, and prevent the request to db depending on some condition.",image:"/ogs/hooks.png"},r="Hooks",d={},l=[{value:"Performance notice",id:"performance-notice",level:3},{value:"Initial data for edit page flow",id:"initial-data-for-edit-page-flow",level:2},{value:"Saving data on edit page",id:"saving-data-on-edit-page",level:2},{value:"Saving data on create page",id:"saving-data-on-create-page",level:2},{value:"Example: modify the created object before it is saved to the database",id:"example-modify-the-created-object-before-it-is-saved-to-the-database",level:3},{value:"List page flow",id:"list-page-flow",level:2},{value:"Example: limit access in list to user-related records",id:"example-limit-access-in-list-to-user-related-records",level:3},{value:"Modify record after it is returned from database",id:"modify-record-after-it-is-returned-from-database",level:3},{value:"Dropdown list of foreignResource",id:"dropdown-list-of-foreignresource",level:3},{value:"Show page flow",id:"show-page-flow",level:2},{value:"Example show limiting:",id:"example-show-limiting",level:3},{value:"All hooks",id:"all-hooks",level:2}];function c(e){const t={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",header:"header",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.header,{children:(0,n.jsx)(t.h1,{id:"hooks",children:"Hooks"})}),"\n",(0,n.jsx)(t.p,{children:"Hooks are used to:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"modify the data before it is saved to the database on create or update"}),"\n",(0,n.jsx)(t.li,{children:"execute something after data were saved or deleted"}),"\n",(0,n.jsx)(t.li,{children:"change the query before fetching items from the database"}),"\n",(0,n.jsx)(t.li,{children:"modify the fetched data before it is displayed in the list and show"}),"\n",(0,n.jsxs)(t.li,{children:["prevent the request to db depending on some condition (Better use ",(0,n.jsx)(t.a,{href:"/docs/tutorial/Customization/limitingAccess",children:"allowedActions"})," for this)"]}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Every hook is executed when AdminForth frontend (Vue SPA) makes some internal API HTTP request to the backend. Every hook function is always executed only on backend side (Node.js) from the HTTP request handler and allows you to perform some actions before or after the actual request to datasource (database) is made. This is most flexible way to control flow and extend it with custom logic."}),"\n",(0,n.jsx)(t.p,{children:"Every hook must return one of two objects:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsxs)(t.li,{children:["If everything is fine and request flow should be continued hook should return ",(0,n.jsx)(t.code,{children:"{ ok: true }"})]}),"\n",(0,n.jsxs)(t.li,{children:["If for some reason you need to interrupt request flow in hook you should return ",(0,n.jsx)(t.code,{children:"{ ok: false, error: 'some error message for user' }"}),". This is\nhandy for access-related tasks, though most of such tasks should be solved with ",(0,n.jsx)(t.a,{href:"/docs/tutorial/Customization/limitingAccess",children:"allowedActions"})," and not hooks."]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:["Every hook is array of async functions, so you can have multiple hooks for one event.\nFor simplicity of course you can specify hook as scalar async function and not as array, but internally it will be anyway converted to array with single element just after app start. Plugins can push new own hooks in front of yours (using ",(0,n.jsx)(t.code,{children:"unshift"}),") or after yours (using ",(0,n.jsx)(t.code,{children:"push"}),"). For example audit log plugin adds hooks for registration of all changes in the database."]}),"\n",(0,n.jsx)(t.p,{children:"Here we will consider possible flows one by one"}),"\n",(0,n.jsx)(t.h3,{id:"performance-notice",children:"Performance notice"}),"\n",(0,n.jsxs)(t.p,{children:["Every hook function is async, so you can use ",(0,n.jsx)(t.code,{children:"await"})," inside it to perform some async operations like fetching data from another service or database, but please remember that while hook will not finish its execution, the request flow will be waiting for it. So every delay awaited in hook will delay the whole request. That is why we encourage you to use parallel async operations in hooks (like Promise.all) to make them faster."]}),"\n",(0,n.jsxs)(t.p,{children:["If multiple hooks are defined (e.g. plugin might add own hook to ",(0,n.jsx)(t.code,{children:"list.beforeDatasourceRequest"})," after you will already add one in your config), then hooks will be executed one by one, and can't be parallelized. This ensures that different hooks will not interfere with each other, but also means that if you have bootleneck in one hook, all other hooks will wait for it and whole request will be slower."]}),"\n",(0,n.jsx)(t.h2,{id:"initial-data-for-edit-page-flow",children:"Initial data for edit page flow"}),"\n",(0,n.jsx)(t.p,{children:"When user opens edit page, AdminForth makes a request to the backend to get the initial data for the form."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Initial data for edit page flow",src:s(3233).A+"",width:"2654",height:"2354"})}),"\n",(0,n.jsxs)(t.p,{children:["Practically you can use ",(0,n.jsx)(t.code,{children:"show.afterDatasourceResponse"})," to modify or add some data before it is displayed on the edit page."]}),"\n",(0,n.jsxs)(t.p,{children:["For example ",(0,n.jsx)(t.a,{href:"/docs/tutorial/Plugins/upload/",children:"upload plugin"})," uses this hook to generate signed preview URL so user can see existing uploaded file preview in form, and at the same time database stores only original file path which might be not accessible without presigned URL."]}),"\n",(0,n.jsx)(t.h2,{id:"saving-data-on-edit-page",children:"Saving data on edit page"}),"\n",(0,n.jsx)(t.p,{children:'When user clicks the "Save" button on edit page, AdminForth makes a request to the backend to save the data.'}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Saving data on edit page",src:s(7605).A+"",width:"2645",height:"2362"})}),"\n",(0,n.jsxs)(t.p,{children:["Practically you can use ",(0,n.jsx)(t.code,{children:"edit.beforeSave"})," hook to modify the data or populate new fields before it is saved to the database."]}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsxs)(t.p,{children:["\ud83d\udc46 Note: according to diagram you should understand that interrupting flow from ",(0,n.jsx)(t.code,{children:"edit.afterSave"})," does not prevent data modification in DB"]}),"\n"]}),"\n",(0,n.jsx)(t.h2,{id:"saving-data-on-create-page",children:"Saving data on create page"}),"\n",(0,n.jsx)(t.p,{children:'When user clicks the "Save" button from create page, AdminForth makes a request to the backend to create new record.'}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Saving data on create page",src:s(8508).A+"",width:"2402",height:"2601"})}),"\n",(0,n.jsx)(t.h3,{id:"example-modify-the-created-object-before-it-is-saved-to-the-database",children:"Example: modify the created object before it is saved to the database"}),"\n",(0,n.jsxs)(t.p,{children:["Let's add reference to ",(0,n.jsx)(t.code,{children:"adminUser"})," when user creates a new apartment:"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",metastring:"title='./resources/apartments.ts'",children:"// diff-add\nimport type { AdminUser } from 'adminforth';\n\n{\n ...\n resourceId: 'aparts',\n columns: [\n ...\n {\n name: 'realtor_id',\n ...\n//diff-add\n showIn: { // don't even show this field in create\n//diff-add\n create: false,\n//diff-add\n filter: false,\n//diff-add\n },\n ...\n },\n ...\n ],\n ...\n//diff-add\n hooks: {\n//diff-add\n create: {\n//diff-add\n beforeSave: async ({ adminUser, record }: { adminUser: AdminUser, record: any }) => {\n//diff-add\n record.realtor_id = adminUser.dbUser.id;\n//diff-add\n return { ok: true };\n//diff-add\n }\n//diff-add\n }\n//diff-add\n }\n}\n"})}),"\n",(0,n.jsx)(t.p,{children:"In this way user who creates the apartment will be assigned as a realtor. Also user can't set other realtor then himself, even if he will make request using curl/devtools because hook will override the value."}),"\n",(0,n.jsx)(t.h2,{id:"list-page-flow",children:"List page flow"}),"\n",(0,n.jsx)(t.p,{children:"When user opens the list page, AdminForth makes a request to the backend to get the list of items."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"List page flow",src:s(7196).A+"",width:"2660",height:"2349"})}),"\n",(0,n.jsx)(t.h3,{id:"example-limit-access-in-list-to-user-related-records",children:"Example: limit access in list to user-related records"}),"\n",(0,n.jsx)(t.p,{children:"For example we can prevent the user to see Apartments created by other users. Superadmin user still can see all:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",metastring:"title='./resources/apartments.ts'",children:'{\n ...\n hooks: {\n list: {\n beforeDatasourceRequest: async ({\n query, adminUser, resource,\n }: {\n query: any; adminUser: AdminUser; resource: AdminForthResource;\n }) => {\n if (adminUser.dbUser.role === "superadmin") {\n return { ok: true };\n }\n if (!query.filters || query.filters.length === 0) {\n query.filters = [];\n }\n // skip existing realtor_id filter if it comes from UI Filters (right panel)\n query.filters = query.filters.filter((filter: any) => filter.field !== "realtor_id");\n query.filters.push({\n field: "realtor_id",\n value: adminUser.dbUser.id,\n operator: "eq",\n });\n return { ok: true };\n },\n },\n },\n}\n'})}),"\n",(0,n.jsx)(t.p,{children:"This hook will prevent the user to see Apartments created by other users in list, however if user will be able to discover\nthe apartment id, he will be able to use show page to see the apartment details, that is why separate limiting for show page is required as well. Below we will discover how to limit access to show page."}),"\n",(0,n.jsx)(t.h3,{id:"modify-record-after-it-is-returned-from-database",children:"Modify record after it is returned from database"}),"\n",(0,n.jsx)(t.p,{children:"You can also change resource data after it was loaded."}),"\n",(0,n.jsx)(t.p,{children:"For example, you can change the way columns value is displayed by changing the value itself:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",metastring:"title='./resources/apartments.ts'",children:'{\n ...\n hooks: {\n list: {\n//diff-add\n afterDatasourceResponse: async ({ response }: { response: any }) => {\n//diff-add\n response.forEach((r: any) => {\n//diff-add\n r.price = `$${r.price}`;\n//diff-add\n });\n//diff-add\n return { ok: true, error: "" };\n//diff-add\n },\n },\n },\n}\n'})}),"\n",(0,n.jsx)(t.h3,{id:"dropdown-list-of-foreignresource",children:"Dropdown list of foreignResource"}),"\n",(0,n.jsxs)(t.p,{children:["By default if there is ",(0,n.jsx)(t.code,{children:"foreignResource"})," like we use for demo on ",(0,n.jsx)(t.code,{children:"realtor_id"})," column, the filter will suggest a\nselect dropdown with list of all Realtors."]}),"\n",(0,n.jsx)(t.p,{children:"This might bring us a leak where explorer will get id's of other users in the system which might be not desired"}),"\n",(0,n.jsx)(t.p,{children:"Let's limit it:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",metastring:"title='./resources/apartments.ts'",children:'{\n ...\n hooks: {\n dropdownList: {\n beforeDatasourceRequest: async ({ adminUser, query }: { adminUser: AdminUser, query: any }) => {\n if (adminUser.dbUser.role !== "superadmin") {\n query.filters = [{field: "id", value: adminUser.dbUser.id, operator: "eq"}];\n };\n return {\n "ok": true,\n };\n }\n },\n },\n}\n'})}),"\n",(0,n.jsx)(t.p,{children:"In our case we limit the dropdown list to show only the current user, however you can use same sample to list only objects who are related to the current user in case if you will have relation configurations which require to show related objects which belongs to the current user."}),"\n",(0,n.jsx)(t.p,{children:"Flow diagram for dropdown list:"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Flow diagram for dropdown list",src:s(3109).A+"",width:"2866",height:"2180"})}),"\n",(0,n.jsx)(t.h2,{id:"show-page-flow",children:"Show page flow"}),"\n",(0,n.jsx)(t.p,{children:"When user opens the show page, AdminForth makes a request to the backend to get the item. This request ia absolutely the same as one for edit initial data, because naturally for most of cases data for show page are the same as initial data for edit page."}),"\n",(0,n.jsxs)(t.p,{children:["However if you still need to distinguish between these two cases you can use ",(0,n.jsx)(t.code,{children:"query.source"})," parameter in hook (we do not mentioned it in diagram for simplicity and rare demand)."]}),"\n",(0,n.jsx)(t.p,{children:"Here is show request flow:"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Here is show request",src:s(2667).A+"",width:"2654",height:"2354"})}),"\n",(0,n.jsx)(t.h3,{id:"example-show-limiting",children:"Example show limiting:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-ts",metastring:"title='./resources/apartments.ts'",children:'{\n ...\n hooks: {\n show: {\n afterDatasourceResponse: async ({\n adminUser, response,\n }: {\n adminUser: AdminUser; response: any;\n }) => {\n if (adminUser.dbUser.role === "superadmin") {\n return { ok: true, response };\n }\n if (response[0].realtor_id.pk !== adminUser.dbUser.id) {\n return { ok: false, error: "You are not allowed to see this record" };\n }\n return { ok: true, response };\n }\n }\n }\n}\n'})}),"\n",(0,n.jsxs)(t.blockquote,{children:["\n",(0,n.jsxs)(t.p,{children:["\ud83d\udc46 Please note that we use ",(0,n.jsx)(t.code,{children:"response[0].realtor_id.pk"})," because this field has ",(0,n.jsx)(t.code,{children:"foreignResource"})," in column option is set\nOtherwise you would use just ",(0,n.jsx)(t.code,{children:"response[0].realtor_id"})]}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Important notice: Using hook to filter out list of items for list page or list of items for dropdown makes a lot of sense because gives ability to change filter of database request. However using hook for show page is not reasonable:"}),"\n",(0,n.jsxs)(t.p,{children:["First of all it semantically better aligns with using ",(0,n.jsx)(t.code,{children:"allowedActions"})," interface. For this particular case you must use ",(0,n.jsx)(t.a,{href:"/docs/tutorial/Customization/limitingAccess#disable-showing-the-resource-based-on-owner",children:"allowedActions.show"})]}),"\n",(0,n.jsxs)(t.p,{children:["Secondly limiting access from this hook will not prevent executing other hooks (e.g. ",(0,n.jsx)(t.code,{children:"beforeDatasourceRequest"}),"), when allowedActions check\nalways performed before any hooks and any database requests."]}),"\n",(0,n.jsx)(t.h2,{id:"all-hooks",children:"All hooks"}),"\n",(0,n.jsxs)(t.p,{children:["Check all hooks in the ",(0,n.jsx)(t.a,{href:"/docs/api/Back/interfaces/AdminForthResource",children:"API reference"}),"."]})]})}function h(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},8508:(e,t,s)=>{s.d(t,{A:()=>o});const o=s.p+"assets/images/image-26-7598a1b3f615891ef5e84c522293075b.png"},7605:(e,t,s)=>{s.d(t,{A:()=>o});const o=s.p+"assets/images/image-27-a518e352285420df9a5089f6de1c8910.png"},2667:(e,t,s)=>{s.d(t,{A:()=>o});const o=s.p+"assets/images/image-29-d92c2448ab9eea2bba5fe09bed7b2d8b.png"},3109:(e,t,s)=>{s.d(t,{A:()=>o});const o=s.p+"assets/images/image-30-0a2b86ef7a702915657e80be98b1aa6c.png"},7196:(e,t,s)=>{s.d(t,{A:()=>o});const o=s.p+"assets/images/image-31-d822389982d88936ab4cc0a3faeb564f.png"},3233:(e,t,s)=>{s.d(t,{A:()=>o});const o=s.p+"assets/images/initial_edit_data_flow-d3f2bff1b9250456ae1420cbdd61af62.png"},8453:(e,t,s)=>{s.d(t,{R:()=>a,x:()=>r});var o=s(6540);const n={},i=o.createContext(n);function a(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:a(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]);