-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy path1002ff02.c464f33e.js
More file actions
1 lines (1 loc) · 75 KB
/
1002ff02.c464f33e.js
File metadata and controls
1 lines (1 loc) · 75 KB
1
(window.webpackJsonp=window.webpackJsonp||[]).push([[5],{113:function(e,t,n){"use strict";n.d(t,"a",(function(){return d})),n.d(t,"b",(function(){return u}));var a=n(0),r=n.n(a);function l(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?i(Object(n),!0).forEach((function(t){l(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):i(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function b(e,t){if(null==e)return{};var n,a,r=function(e,t){if(null==e)return{};var n,a,r={},l=Object.keys(e);for(a=0;a<l.length;a++)n=l[a],t.indexOf(n)>=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);for(a=0;a<l.length;a++)n=l[a],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var c=r.a.createContext({}),p=function(e){var t=r.a.useContext(c),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},d=function(e){var t=p(e.components);return r.a.createElement(c.Provider,{value:t},e.children)},m={inlineCode:"code",wrapper:function(e){var t=e.children;return r.a.createElement(r.a.Fragment,{},t)}},s=r.a.forwardRef((function(e,t){var n=e.components,a=e.mdxType,l=e.originalType,i=e.parentName,c=b(e,["components","mdxType","originalType","parentName"]),d=p(n),s=a,u=d["".concat(i,".").concat(s)]||d[s]||m[s]||l;return n?r.a.createElement(u,o(o({ref:t},c),{},{components:n})):r.a.createElement(u,o({ref:t},c))}));function u(e,t){var n=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var l=n.length,i=new Array(l);i[0]=s;var o={};for(var b in t)hasOwnProperty.call(t,b)&&(o[b]=t[b]);o.originalType=e,o.mdxType="string"==typeof e?e:a,i[1]=o;for(var c=2;c<l;c++)i[c]=n[c];return r.a.createElement.apply(null,i)}return r.a.createElement.apply(null,n)}s.displayName="MDXCreateElement"},68:function(e,t,n){"use strict";n.r(t),n.d(t,"frontMatter",(function(){return i})),n.d(t,"metadata",(function(){return o})),n.d(t,"toc",(function(){return b})),n.d(t,"default",(function(){return p}));var a=n(3),r=n(7),l=(n(0),n(113)),i={id:"generic-controller",title:"Generic Controller"},o={unversionedId:"generic-controller",id:"generic-controller",isDocsHomePage:!1,title:"Generic Controller",description:"Generic controller is a common Plumier controller with generic type signature, it take advantage of inheritance to possibly serve CRUD API based on ORM entity.",source:"@site/docs/Generic-Controller.md",slug:"/generic-controller",permalink:"/generic-controller",editUrl:"https://github.com/plumier/plumier/edit/master/docs/docusaurus/docs/Generic-Controller.md",version:"current",sidebar:"overview",previous:{title:"Security",permalink:"/security"},next:{title:"TypeORM Helper",permalink:"/typeorm-helper"}},b=[{value:"Enable Functionality",id:"enable-functionality",children:[]},{value:"Mark Entity Handled by Generic Controller",id:"mark-entity-handled-by-generic-controller",children:[]},{value:"Getting and Saving Simple Relation",id:"getting-and-saving-simple-relation",children:[]},{value:"Getting and Saving Array Relation",id:"getting-and-saving-array-relation",children:[]},{value:"Apply Multiple Decorators",id:"apply-multiple-decorators",children:[]},{value:"Filters",id:"filters",children:[]},{value:"Delete Column",id:"delete-column",children:[]},{value:"Query Strings",id:"query-strings",children:[]},{value:"Custom Path Name",id:"custom-path-name",children:[]},{value:"Control Access To The Entity Properties",id:"control-access-to-the-entity-properties",children:[]},{value:"Control Access To The Generated Routes",id:"control-access-to-the-generated-routes",children:[]},{value:"Ignore Some Routes",id:"ignore-some-routes",children:[]},{value:"Transform Response",id:"transform-response",children:[]},{value:"Custom Query",id:"custom-query",children:[]},{value:"Entity Authorization Policy",id:"entity-authorization-policy",children:[]},{value:"Request Hook",id:"request-hook",children:[]},{value:"Use Custom Generic Controller",id:"use-custom-generic-controller",children:[]}],c={toc:b};function p(e){var t=e.components,n=Object(r.a)(e,["components"]);return Object(l.b)("wrapper",Object(a.a)({},c,n,{components:t,mdxType:"MDXLayout"}),Object(l.b)("p",null,"Generic controller is a common Plumier controller with generic type signature, it take advantage of inheritance to possibly serve CRUD API based on ORM entity. "),Object(l.b)("h2",{id:"enable-functionality"},"Enable Functionality"),Object(l.b)("p",null,"Generic controller supported TypeORM and Mongoose (with Plumier mongoose helper) entities. Enable the generic controller by installing the ",Object(l.b)("inlineCode",{parentName:"p"},"TypeORMFacility")," or ",Object(l.b)("inlineCode",{parentName:"p"},"MongooseFacility")," on the Plumier application. "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{6,7}","{6,7}":!0},'import Plumier, { WebApiFacility } from "plumier"\nimport { TypeORMFacility } from "@plumier/typeorm"\n\nnew Plumier()\n .set(new WebApiFacility())\n .set(new TypeORMFacility())\n .set(new ControllerFacility({ controller: "<entities path or glob>" }))\n')),Object(l.b)("p",null,"Or if you are using mongoose helper like below"),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{6,7}","{6,7}":!0},'import Plumier, { WebApiFacility } from "plumier"\nimport { MongooseFacility } from "@plumier/mongoose"\n\nnew Plumier()\n .set(new WebApiFacility())\n .set(new MongooseFacility())\n .set(new ControllerFacility({ controller: "<entities path or glob>" }))\n')),Object(l.b)("p",null,"Above facilities is a common facility used if you are using TypeORM or Mongoose with Plumier. In context of generic controller above facilities will normalize entities to make it ready used by generic controller helpers. "),Object(l.b)("h2",{id:"mark-entity-handled-by-generic-controller"},"Mark Entity Handled by Generic Controller"),Object(l.b)("p",null,"After installing facility above you need to mark specific entity that will be generated into CRUD API with ",Object(l.b)("inlineCode",{parentName:"p"},"@route.controller()")," like below: "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{4}","{4}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route } from "plumier"\n\n@route.controller()\n@Entity()\nclass User {\n @PrimaryGeneratedColumn()\n id: number\n\n @Column()\n email: string\n\n @Column()\n name: string\n}\n')),Object(l.b)("p",null,"Or if you using Mongoose helper "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{4}","{4}":!0},'import { collection } from "@plumier/mongoose"\nimport { route } from "plumier"\n\n@route.controller()\n@collection()\nclass User {\n constructor(\n public id: string,\n public email: string,\n public name: string\n ){}\n}\n')),Object(l.b)("p",null,"Above code will generate six routes handled by generic controller implementation. "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users")),Object(l.b)("td",{parentName:"tr",align:null},"Add new user")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id?select")),Object(l.b)("td",{parentName:"tr",align:null},"Get user by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Replace user by ID (required validation used)")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Modify user by ID (required validation ignored)")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Delete user by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users")),Object(l.b)("td",{parentName:"tr",align:null},"Get list of users with pagination, order, filter and projection")))),Object(l.b)("p",null,"'''info\nSwagger supported generic controller, since its just a common controller with a generic signature (make sure to enable the swagger functionality by installing ",Object(l.b)("inlineCode",{parentName:"p"},"SwaggerFacility"),").\n'''"),Object(l.b)("h2",{id:"getting-and-saving-simple-relation"},"Getting and Saving Simple Relation"),Object(l.b)("p",null,"Relational data with single value (one to one or many to one) by default will be populated on each request. For example if we have entity below: "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{24}","{24}":!0},"@route.controller()\n@Entity()\nclass Address {\n \n /** other columns **/\n\n @Column()\n city:string\n\n @Column()\n address:string\n}\n\n@route.controller()\n@Entity()\nclass User {\n \n /** other columns **/\n\n @Column()\n email:string\n\n @OneToOne(x => Address)\n address:Address\n}\n")),Object(l.b)("p",null,"Above code generates 6 routes for each ",Object(l.b)("inlineCode",{parentName:"p"},"/address")," and ",Object(l.b)("inlineCode",{parentName:"p"},"/users"),". ",Object(l.b)("inlineCode",{parentName:"p"},"User")," entity contains relation property to ",Object(l.b)("inlineCode",{parentName:"p"},"Address")," entity which is a one to one relation. Issuing ",Object(l.b)("inlineCode",{parentName:"p"},"GET /users/:id")," will automatically populate the address, thus the response will be like below"),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-json"},'{\n "email": "john.doe@gmail.com",\n // full address object populated\n "address": {\n "city": "Badung",\n "address": "Jl Surapati No. 19 Kuta" \n }\n}\n')),Object(l.b)("p",null,"While ",Object(l.b)("inlineCode",{parentName:"p"},"GET /users/:id")," returns the full address object, saving address (POST, PUT, PATCH) only require the ID of the address like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-json"},'POST /users/123 \n\n{\n "email": "john.doe@gmail.com",\n "address": 456 //<-- address ID\n}\n')),Object(l.b)("h2",{id:"getting-and-saving-array-relation"},"Getting and Saving Array Relation"),Object(l.b)("p",null,"For array relation (one to many relation), Plumier provide a nested route to easily perform CRUD operation on child relation. "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{9}","{9}":!0},"@Entity()\nclass User {\n \n /** other columns **/\n\n @Column()\n name:string\n\n @route.controller()\n @OneToMany(x => Email, x => x.user)\n emails:Email[]\n}\n\n@Entity()\nclass Email {\n \n /** other columns **/\n\n @Column()\n email:string\n\n @Column()\n description:string\n\n @ManyToOne(x => User, x => x.emails)\n user:User\n}\n")),Object(l.b)("p",null,"Above code showing that we apply ",Object(l.b)("inlineCode",{parentName:"p"},"@route.controller()")," on the ",Object(l.b)("inlineCode",{parentName:"p"},"User.emails")," relation. Using this setup will make Plumier generate a nested routes like below "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails")),Object(l.b)("td",{parentName:"tr",align:null},"Add new user's email")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id?select")),Object(l.b)("td",{parentName:"tr",align:null},"Get user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Replace user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Modify user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Delete user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails")),Object(l.b)("td",{parentName:"tr",align:null},"Get list of user's emails with paging, filter, order and projection")))),Object(l.b)("div",{className:"admonition admonition-note alert alert--secondary"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"14",height:"16",viewBox:"0 0 14 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"}))),"note")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"If you separate your code per entity, you may found that configuring on relation is not so clean, because your controller configuration is not in your entity. You can keep specify decorator on the child entity but use ",Object(l.b)("inlineCode",{parentName:"p"},"useNested")," configuration like below."),Object(l.b)("pre",{parentName:"div"},Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'@route.controller(c => c.useNested(User, "emails"))\n@Entity()\nclass Email {\n \n}\n')),Object(l.b)("p",{parentName:"div"},"The result will be the same as above"))),Object(l.b)("h2",{id:"apply-multiple-decorators"},"Apply Multiple Decorators"),Object(l.b)("p",null,"Its possible to apply multiple ",Object(l.b)("inlineCode",{parentName:"p"},"@route.controller()")," decorator on entity or entity relation, but the generated route must be unique or the route generator static check will shows errors. "),Object(l.b)("p",null,"For example on previous users -> email entity you may need to show ",Object(l.b)("inlineCode",{parentName:"p"},"/users/:uid/emails")," but you may also wants to enable API to list all emails ",Object(l.b)("inlineCode",{parentName:"p"},"/emails")," "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'@Entity()\nclass User {\n \n /** other columns **/\n\n @Column()\n name:string\n\n @OneToMany(x => Email, x => x.user)\n emails:Email[]\n}\n\n@route.controller(c => c.useNested(User, "emails"))\n@route.controller(c => c.actions("Delete", "GetOne", "Patch", "Post", "Put").ignore())\n@Entity()\nclass Email {\n \n /** other columns **/\n\n @Column()\n email:string\n\n @Column()\n description:string\n\n @ManyToOne(x => User, x => x.emails)\n user:User\n}\n')),Object(l.b)("p",null,"Above will generates 7 routes like below "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails")),Object(l.b)("td",{parentName:"tr",align:null},"Add new user's email")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id?select")),Object(l.b)("td",{parentName:"tr",align:null},"Get user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Replace user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Modify user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Delete user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails")),Object(l.b)("td",{parentName:"tr",align:null},"Get list of user's emails with paging, filter, order and projection")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/emails")),Object(l.b)("td",{parentName:"tr",align:null},"Get list of all emails")))),Object(l.b)("h2",{id:"filters"},"Filters"),Object(l.b)("p",null,"Generic controller provided filter query string to narrow API response. To be able to filter specific field, the appropriate property needs to be authorized. "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{11,16}","{11,16}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route } from "plumier"\n\n@route.controller()\n@Entity()\nclass User {\n @PrimaryGeneratedColumn()\n id: number\n\n // authorize name field to be search able by everyone\n @authorize.filter()\n @Column()\n name: string\n\n // authorize email field to be search able by admin\n @authorize.filter("Admin")\n @Column()\n email: string\n}\n')),Object(l.b)("p",null,"Using above code enabled us to query the response result like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre"},"# filter by email\nGET /users?filter[email]=john.doe@gmail.com\n\n# filter by name, will return all users name start with john\nGET /users?filter[name]=john*\n\n# combine both filter, will return with AND operator\nGET /users?filter[email]=john.doe@gmail.com&filter[name]=john\n")),Object(l.b)("p",null,"Several filter supported based on property data type "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Filter"),Object(l.b)("th",{parentName:"tr",align:null},"Description"),Object(l.b)("th",{parentName:"tr",align:null},"Data Type"),Object(l.b)("th",{parentName:"tr",align:null},"Example"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"Equal"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by exact value"),Object(l.b)("td",{parentName:"tr",align:null},"All"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=3"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"Partial"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by partial value using ",Object(l.b)("inlineCode",{parentName:"td"},"*")," (at beginning/end)"),Object(l.b)("td",{parentName:"tr",align:null},"String"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[name]=john*"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"Range"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by range with pattern ",Object(l.b)("inlineCode",{parentName:"td"},"start...end")),Object(l.b)("td",{parentName:"tr",align:null},"Date, Number"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=1...18"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GTE"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by greater than or equal using ",Object(l.b)("inlineCode",{parentName:"td"},">=")),Object(l.b)("td",{parentName:"tr",align:null},"Date, Number"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=>=20"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"LTE"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by less than or equal using ",Object(l.b)("inlineCode",{parentName:"td"},"<=")),Object(l.b)("td",{parentName:"tr",align:null},"Date, Number"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=<=20"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GT"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by greater than using ",Object(l.b)("inlineCode",{parentName:"td"},">")),Object(l.b)("td",{parentName:"tr",align:null},"Date, Number"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=>20"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"LT"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by less than using ",Object(l.b)("inlineCode",{parentName:"td"},"<")),Object(l.b)("td",{parentName:"tr",align:null},"Date, Number"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=<20"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"Not Equal"),Object(l.b)("td",{parentName:"tr",align:null},"Filter by not equal using ",Object(l.b)("inlineCode",{parentName:"td"},"!")),Object(l.b)("td",{parentName:"tr",align:null},"All"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users?filter[age]=!20"))))),Object(l.b)("h2",{id:"delete-column"},"Delete Column"),Object(l.b)("p",null,"By default when you perform ",Object(l.b)("inlineCode",{parentName:"p"},"DELETE /users/{id}")," it will delete the user record permanently from the database, You can specify the delete flag by providing ",Object(l.b)("inlineCode",{parentName:"p"},"@entity.deleteColumn()")," decorator above the flag property with ",Object(l.b)("inlineCode",{parentName:"p"},"boolean")," datatype like below."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{10}","{10}":!0},'import { entity } from "plumier"\n\n@Entity()\nexport class User {\n @PrimaryGeneratedColumn()\n id: number\n\n /** other properties **/\n\n @entity.deleteColumn()\n @Column({ default: false })\n deleted: boolean\n}\n')),Object(l.b)("p",null,"Using above code when you request ",Object(l.b)("inlineCode",{parentName:"p"},"DELETE /users/{id}")," Plumier will set the ",Object(l.b)("inlineCode",{parentName:"p"},"deleted")," property into ",Object(l.b)("inlineCode",{parentName:"p"},"true")," automatically."),Object(l.b)("h2",{id:"query-strings"},"Query Strings"),Object(l.b)("p",null,"Both get by id and get list route has some extra query string to manipulate the response match your need. "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Query String"),Object(l.b)("th",{parentName:"tr",align:null},"Example"),Object(l.b)("th",{parentName:"tr",align:null},"Default"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"select")),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"GET /users?select=name,email,dob")),Object(l.b)("td",{parentName:"tr",align:null},"All properties"),Object(l.b)("td",{parentName:"tr",align:null},"Select properties to include in JSON response")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"limit")),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"GET /users?limit=20")),Object(l.b)("td",{parentName:"tr",align:null},"50"),Object(l.b)("td",{parentName:"tr",align:null},"Limit number of records returned in response")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"offset")),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"GET /users?offset=3")),Object(l.b)("td",{parentName:"tr",align:null},"0"),Object(l.b)("td",{parentName:"tr",align:null},"Offset of the record set returned in response")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"filter")),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"GET /users?filter[name]=john&filter[active]=true")),Object(l.b)("td",{parentName:"tr",align:null},"-"),Object(l.b)("td",{parentName:"tr",align:null},"Find records by property using exact comparison")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"order")),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"GET /users?order=-createdAt,name")),Object(l.b)("td",{parentName:"tr",align:null},"-"),Object(l.b)("td",{parentName:"tr",align:null},"Order by properties, ",Object(l.b)("inlineCode",{parentName:"td"},"-")," for descending order")))),Object(l.b)("p",null,"Above query string supported by generic controller and nested generic controller. "),Object(l.b)("h4",{id:"get-by-id"},"Get By ID"),Object(l.b)("p",null,"Get by ID for generic controller and nested generic controller supported ",Object(l.b)("inlineCode",{parentName:"p"},"select")," query string like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre"},"GET /users/:id?select=name,email,dob\nGET /users/:pid/pets/:id?select=name,active,dob\n")),Object(l.b)("h4",{id:"get-list"},"Get List"),Object(l.b)("p",null,"Get list supported all the query string, it can be combined in single request "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre"},"GET /users?select=name,email,dob&filter[email]=john.doe@gmail.com&order=-createdAt,name&offset=5\nGET /users/:pid/pets/:id?select=name,dob&filter[name]=bingo&order=-createdAt,name&offset=5\n")),Object(l.b)("h2",{id:"custom-path-name"},"Custom Path Name"),Object(l.b)("p",null,"Plumier provide a default route path name based on entity name (pluralized), you can specify a new path name by provide it on the ",Object(l.b)("inlineCode",{parentName:"p"},"@route.controller()")," parameter. "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{4}","{4}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route } from "plumier"\n\n@route.controller("user-data/:uid")\n@Entity()\nclass User {\n @PrimaryGeneratedColumn()\n id: number\n\n @Column()\n email: string\n\n @Column()\n name: string\n}\n')),Object(l.b)("p",null,"Above code showing that we specify custom path name ",Object(l.b)("inlineCode",{parentName:"p"},"user-data/:uid"),", it will generate routes like below"),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data")),Object(l.b)("td",{parentName:"tr",align:null},"Add new user")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid")),Object(l.b)("td",{parentName:"tr",align:null},"Get user by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid")),Object(l.b)("td",{parentName:"tr",align:null},"Replace user by ID (required validation used)")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid")),Object(l.b)("td",{parentName:"tr",align:null},"Modify user by ID (required validation ignored)")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid")),Object(l.b)("td",{parentName:"tr",align:null},"Delete user by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data")),Object(l.b)("td",{parentName:"tr",align:null},"Get list of users with paging, filter, order and projection")))),Object(l.b)("p",null,"For nested generic controller you need to specify ID for parent and the child "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{6}","{6}":!0},'@Entity()\nclass User {\n \n /** other columns **/\n\n @route.controller("user-data/:uid/email-data/:eid")\n @OneToMany(x => Email, x => x.user)\n emails:Email[]\n}\n')),Object(l.b)("p",null,"Above code will generate routes below "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid/email-data")),Object(l.b)("td",{parentName:"tr",align:null},"Add new user's email")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid/email-data/:eid")),Object(l.b)("td",{parentName:"tr",align:null},"Get user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid/email-data/:eid")),Object(l.b)("td",{parentName:"tr",align:null},"Replace user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid/email-data/:eid")),Object(l.b)("td",{parentName:"tr",align:null},"Modify user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid/email-data/:eid")),Object(l.b)("td",{parentName:"tr",align:null},"Delete user's email by ID")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/user-data/:uid/email-data")),Object(l.b)("td",{parentName:"tr",align:null},"Get list of user's email with paging, filter, order and projection")))),Object(l.b)("h2",{id:"control-access-to-the-entity-properties"},"Control Access To The Entity Properties"),Object(l.b)("p",null,"Plumier provide functionality to protect your data easily, you can use ",Object(l.b)("inlineCode",{parentName:"p"},"@authorize")," decorator to authorize user to write or read your entity property. "),Object(l.b)("div",{className:"admonition admonition-info alert alert--info"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"14",height:"16",viewBox:"0 0 14 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"}))),"info")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"Refer to ",Object(l.b)("a",{parentName:"p",href:"/security"},"Security")," on how to setup user authorization on your Plumier application "))),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{13,20}","{13,20}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route, authorize } from "plumier"\n\n@route.controller()\n@Entity()\nclass User {\n @PrimaryGeneratedColumn()\n id: number\n\n @Column()\n email: string\n\n @authorize.writeonly()\n @Column()\n password: string\n\n @Column()\n name: string\n\n @authorize.write("SuperAdmin", "Admin")\n @Column()\n role: "SuperAdmin" | "Admin" | "User"\n}\n')),Object(l.b)("p",null,"Above code showing that we apply ",Object(l.b)("inlineCode",{parentName:"p"},"@authorize")," decorator on ",Object(l.b)("inlineCode",{parentName:"p"},"password")," and ",Object(l.b)("inlineCode",{parentName:"p"},"role")," property which contains sensitive data. Using above configuration ",Object(l.b)("inlineCode",{parentName:"p"},"password")," will not be visible on any response, and ",Object(l.b)("inlineCode",{parentName:"p"},"role")," only can be set by ",Object(l.b)("inlineCode",{parentName:"p"},"SuperAdmin")," and ",Object(l.b)("inlineCode",{parentName:"p"},"Admin"),". Below list of authorization you can use to protect property of the entity"),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Decorator"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},'@authorize.route("SuperAdmin")')),Object(l.b)("td",{parentName:"tr",align:null},"Protect route can be accessed by specific role (",Object(l.b)("inlineCode",{parentName:"td"},"SuperAdmin"),")")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},'@authorize.write("SuperAdmin")')),Object(l.b)("td",{parentName:"tr",align:null},"Protect property only can be write by specific role (",Object(l.b)("inlineCode",{parentName:"td"},"SuperAdmin"),")")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},'@authorize.read("SuperAdmin")')),Object(l.b)("td",{parentName:"tr",align:null},"Protect property only can be read by specific role (",Object(l.b)("inlineCode",{parentName:"td"},"SuperAdmin"),")")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"@authorize.readonly()")),Object(l.b)("td",{parentName:"tr",align:null},"Protect property only can be read and no other role can write it")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"@authorize.writeonly()")),Object(l.b)("td",{parentName:"tr",align:null},"Protect property only can be write and no other role can read it")))),Object(l.b)("h2",{id:"control-access-to-the-generated-routes"},"Control Access To The Generated Routes"),Object(l.b)("p",null,"You can specify authorization into specific generated route by providing more configuration on the ",Object(l.b)("inlineCode",{parentName:"p"},"@route.controller()")," decorator."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{4}","{4}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route, authorize } from "plumier"\n\n@route.controller(c => c.mutators().authorize("SuperAdmin", "Admin"))\n@Entity()\nclass User {\n \n /** properties / columns */\n\n}\n')),Object(l.b)("p",null,"Above code showing that we apply authorization to the ",Object(l.b)("inlineCode",{parentName:"p"},"mutators()")," http methods. ",Object(l.b)("inlineCode",{parentName:"p"},"mutators()")," is a syntax sugar to specify generic controller actions handled the ",Object(l.b)("inlineCode",{parentName:"p"},"POST"),", ",Object(l.b)("inlineCode",{parentName:"p"},"PUT"),", ",Object(l.b)("inlineCode",{parentName:"p"},"PATCH")," and ",Object(l.b)("inlineCode",{parentName:"p"},"DELETE"),". Using above configuration the result of the generated routes are like below"),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Action"),Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Access"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"save")),Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"get")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Any user")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"replace")),Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"modify")),Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"delete")),Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"list")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users")),Object(l.b)("td",{parentName:"tr",align:null},"Any user")))),Object(l.b)("div",{className:"admonition admonition-info alert alert--info"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"14",height:"16",viewBox:"0 0 14 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"}))),"info")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"If no authorization specified, the default authorization for generated route is ",Object(l.b)("inlineCode",{parentName:"p"},"Authenticated")," means all authenticated user can access the route."))),Object(l.b)("p",null,"For nested routes (one to many relation) you can define the same configuration like above."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{7}","{7}":!0},'@Entity()\nclass User {\n \n /** other columns **/\n\n @route.controller()\n @authorize.route(c => c.mutators().authorize("SuperAdmin", "Admin"))\n @OneToMany(x => Email, x => x.user)\n emails:Email[]\n}\n')),Object(l.b)("p",null,"Using above configuration the route access now is like below "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Action"),Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Access"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"save")),Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"get")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"Any user")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"replace")),Object(l.b)("td",{parentName:"tr",align:null},"PUT"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"modify")),Object(l.b)("td",{parentName:"tr",align:null},"PATCH"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"delete")),Object(l.b)("td",{parentName:"tr",align:null},"DELETE"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id")),Object(l.b)("td",{parentName:"tr",align:null},"SuperAdmin, Admin")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"list")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails")),Object(l.b)("td",{parentName:"tr",align:null},"Any user")))),Object(l.b)("h2",{id:"ignore-some-routes"},"Ignore Some Routes"),Object(l.b)("p",null,"In some case you may want to hide specific route generated. You can use the ",Object(l.b)("inlineCode",{parentName:"p"},"ignore()")," configuration to ignore specific methods"),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{4-8}","{4-8}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route } from "plumier"\n\n@route.controller(c => {\n c.put().ignore()\n c.patch().ignore()\n c.delete().ignore()\n})\n@Entity()\nclass User {\n \n /** properties / columns */\n\n}\n')),Object(l.b)("p",null,"Above code showing that we specify the method one by one instead of using ",Object(l.b)("inlineCode",{parentName:"p"},"mutators()")," like the previous example. The result of the "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Action"),Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"save")),Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"get")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:id"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"list")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users"))))),Object(l.b)("p",null,"It also can be applied on the entity relation (one to many) to ignore some nested routes like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{6-10}","{6-10}":!0},"@Entity()\nclass User {\n \n /** other columns **/\n\n @route.controller(c => {\n c.put().ignore()\n c.patch().ignore()\n c.delete().ignore()\n })\n @OneToMany(x => Email, x => x.user)\n emails:Email[]\n}\n")),Object(l.b)("p",null,"Above code showing that we applied the ignore decorator on the entity relation, it will produce"),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Action"),Object(l.b)("th",{parentName:"tr",align:null},"Method"),Object(l.b)("th",{parentName:"tr",align:null},"Route"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"save")),Object(l.b)("td",{parentName:"tr",align:null},"POST"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"get")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails/:id"))),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"list")),Object(l.b)("td",{parentName:"tr",align:null},"GET"),Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"/users/:pid/emails"))))),Object(l.b)("h2",{id:"transform-response"},"Transform Response"),Object(l.b)("p",null,"Its possible to transform the response result of the ",Object(l.b)("inlineCode",{parentName:"p"},"GET")," method using ",Object(l.b)("inlineCode",{parentName:"p"},"transformer()")," configuration. For example we have entities below"),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},"@Entity()\nexport class Comment {\n @PrimaryGeneratedColumn()\n id: number\n\n @Column()\n message: string\n\n @ManyToOne(x => User)\n user: User\n\n @ManyToOne(x => Todo)\n todo:Todo\n}\n")),Object(l.b)("p",null,"You may want the response of ",Object(l.b)("inlineCode",{parentName:"p"},"GET /users")," only contains properties ",Object(l.b)("inlineCode",{parentName:"p"},"id"),", ",Object(l.b)("inlineCode",{parentName:"p"},"message"),", ",Object(l.b)("inlineCode",{parentName:"p"},"userName")," and ",Object(l.b)("inlineCode",{parentName:"p"},"userPicture"),", which ",Object(l.b)("inlineCode",{parentName:"p"},"userName")," and ",Object(l.b)("inlineCode",{parentName:"p"},"userPicture")," can be retrieved from ",Object(l.b)("inlineCode",{parentName:"p"},"user")," property. "),Object(l.b)("p",null,"First you need to create the model of the transformed entity."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},"export class CommentWithUser {\n @noop()\n id: number\n\n @noop()\n message: string\n\n @noop()\n userName: string\n\n @noop()\n userPicture: string\n}\n")),Object(l.b)("p",null,"Then specify the transformation function using ",Object(l.b)("inlineCode",{parentName:"p"},"transformer")," configuration "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},"@route.controller(c => {\n c.accessors().transformer(CommentWithUser, x => {\n return {\n id: x.id,\n message: x.message,\n userName: x.user.name,\n userPicture: x.user.picture\n }\n })\n})\n@Entity()\nexport class Comment {\n @PrimaryGeneratedColumn()\n id: number\n\n @Column()\n message: string\n\n @ManyToOne(x => User)\n user: User\n\n @ManyToOne(x => Todo)\n todo:Todo\n}\n")),Object(l.b)("p",null,"Above code showing that we specify the ",Object(l.b)("inlineCode",{parentName:"p"},"transformer")," configuration, the first parameter is the target model then the second parameter is the transformation function. "),Object(l.b)("div",{className:"admonition admonition-warning alert alert--danger"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"12",height:"16",viewBox:"0 0 12 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"}))),"warning")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"When using response transform, the ",Object(l.b)("inlineCode",{parentName:"p"},"select")," query may still applied but the response will be based on what you returned in transform function."))),Object(l.b)("h2",{id:"custom-query"},"Custom Query"),Object(l.b)("p",null,"Unlike transform response, with custom query you provide a custom database query for ",Object(l.b)("inlineCode",{parentName:"p"},"getOne")," and/or ",Object(l.b)("inlineCode",{parentName:"p"},"getMany")," method to get different response result than the default generic controller query provided. "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'import { route } from "plumier"\nimport { Entity, Column, PrimaryGeneratedColumn } from "typeorm"\nimport { noop } from "@plumier/reflect"\n// for mongoose use import { transformFilter } from "@plumier/mongoose"\nimport { transformFilter } from "@plumier/typeorm"\n\n@route.controller(c => {\n // custom query for GET /users/:id\n c.getOne().custom(UserDto, async ({ id }) => {\n const repo = getManager().getRepository(User)\n return repo.findOne(id, { select: ["email"] })\n })\n // custom query for GET /users\n c.getMany().custom([UserDto], async ({ limit, offset, filter }) => {\n const repo = getManager().getRepository(User)\n const where = transformFilter(filter)\n return repo.find({ take: limit, skip: offset, where, select: ["email"] })\n })\n})\n@Entity()\nclass User {\n @PrimaryGeneratedColumn()\n id: number\n @Column()\n email: string\n @Column()\n name: string\n}\n// the response will be like this \nclass UserDto {\n @noop()\n email: string\n}\n')),Object(l.b)("p",null,"Using above code you provide a custom query that will be used by the ",Object(l.b)("inlineCode",{parentName:"p"},"GET /users/:id"),". To override query of ",Object(l.b)("inlineCode",{parentName:"p"},"GET /users")," you can use the ",Object(l.b)("inlineCode",{parentName:"p"},"getMany()")," method instead of ",Object(l.b)("inlineCode",{parentName:"p"},"getOne()"),". "),Object(l.b)("p",null,"Signature of the ",Object(l.b)("inlineCode",{parentName:"p"},"custom")," query method is like below "),Object(l.b)("p",null,Object(l.b)("inlineCode",{parentName:"p"},"custom(responseType, queryCallback)")," "),Object(l.b)("ul",null,Object(l.b)("li",{parentName:"ul"},Object(l.b)("inlineCode",{parentName:"li"},"responseType")," is the model used to generate schema of the response. This model used by Open API generator to generate response schema, and also used by response authorization. Important to note that the ",Object(l.b)("inlineCode",{parentName:"li"},"@authorize.read()")," on the entity will not take effect if you use different model than the entity, you need to decorate the appropriate property accordingly. "),Object(l.b)("li",{parentName:"ul"},Object(l.b)("inlineCode",{parentName:"li"},"queryCallback")," is a function returned the database query, signature of the query callback is mostly the same for ",Object(l.b)("inlineCode",{parentName:"li"},"getOne()")," and ",Object(l.b)("inlineCode",{parentName:"li"},"getMany()"),", except the first parameter object, see below.")),Object(l.b)("p",null,"Query callback signature for ",Object(l.b)("inlineCode",{parentName:"p"},"getOne()")," and ",Object(l.b)("inlineCode",{parentName:"p"},"getMany()")," is like below "),Object(l.b)("p",null,Object(l.b)("inlineCode",{parentName:"p"},"(param, ctx) => any")," "),Object(l.b)("ul",null,Object(l.b)("li",{parentName:"ul"},Object(l.b)("inlineCode",{parentName:"li"},"param")," contains the action parameter such as ",Object(l.b)("inlineCode",{parentName:"li"},"id"),", ",Object(l.b)("inlineCode",{parentName:"li"},"limit"),", ",Object(l.b)("inlineCode",{parentName:"li"},"offset")," etc. The parameter is differ between ",Object(l.b)("inlineCode",{parentName:"li"},"getOne()")," and ",Object(l.b)("inlineCode",{parentName:"li"},"getMany()"),"."),Object(l.b)("li",{parentName:"ul"},Object(l.b)("inlineCode",{parentName:"li"},"ctx")," is the request context")),Object(l.b)("h2",{id:"entity-authorization-policy"},"Entity Authorization Policy"),Object(l.b)("p",null,"Entity Authorization Policy (Entity Policy) is a custom ",Object(l.b)("a",{parentName:"p",href:"/security#authorization"},"auth policy")," designed to secure entity data by ID. "),Object(l.b)("p",null,"Entity policy requires unique combination between policy name and the entity type, the definition is like below."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},"entityPolicy(ENTITY_CLASS)\n .register(POLICY_NAME, (auth:AuthorizationContext, id:any) => boolean)\n")),Object(l.b)("ul",null,Object(l.b)("li",{parentName:"ul"},Object(l.b)("inlineCode",{parentName:"li"},"ENTITY_CLASS")," is the entity which authorization policy registered to."),Object(l.b)("li",{parentName:"ul"},Object(l.b)("inlineCode",{parentName:"li"},"POLICY_NAME")," is the policy name, policy name may the same with other entity but must be unique when combined with entity class."),Object(l.b)("li",{parentName:"ul"},"Authorization callback is where you put the authorization logic, return ",Object(l.b)("inlineCode",{parentName:"li"},"true")," or ",Object(l.b)("inlineCode",{parentName:"li"},"Promise<true>")," to allow otherwise ",Object(l.b)("inlineCode",{parentName:"li"},"false")," to restrict.")),Object(l.b)("p",null,"Since the internal process of entity policy is quite complex, its a lot easier to understand entity policy from its implementation. For example we created an entity policy named ",Object(l.b)("inlineCode",{parentName:"p"},"ResourceOwner")," which means the current data is owned by the login user. Then its easy to understand that for entity ",Object(l.b)("inlineCode",{parentName:"p"},"User"),", the logic of ",Object(l.b)("inlineCode",{parentName:"p"},"ResourceOwner")," is when the provided ID by authorization callback is the same as the current login user ID like below. "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'// define policy for User entity\nentityPolicy(User)\n // ResourceOwner is when current login user id is the same as current User id \n .register("ResourceOwner", (auth, id) => auth.user?.userId === id)\n')),Object(l.b)("p",null,"This policy then can be applied on the generic controller configuration builder, to secure specific route like below."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{2}","{2}":!0},'@route.controller(c => {\n c.delete().authorize("ResourceOwner")\n})\n@Entity()\nexport class User {\n @PrimaryGeneratedColumn()\n id:number\n\n @Column()\n email: string\n\n @Column()\n name: string\n}\n')),Object(l.b)("p",null,"By using above configuration only the ",Object(l.b)("inlineCode",{parentName:"p"},"ResourceOwner")," of the User entity allowed to access ",Object(l.b)("inlineCode",{parentName:"p"},"DELETE /users/{id}")," route. By giving ",Object(l.b)("inlineCode",{parentName:"p"},"DELETE /users/12345")," only user with user ID ",Object(l.b)("inlineCode",{parentName:"p"},"12345")," allowed to access it. "),Object(l.b)("p",null,"The policy also can be used to secure property using ",Object(l.b)("inlineCode",{parentName:"p"},"@authorize.read()")," or ",Object(l.b)("inlineCode",{parentName:"p"},"@authorize.write()")," like below."),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{7}","{7}":!0},'@route.controller()\n@Entity()\nexport class User {\n @PrimaryGeneratedColumn()\n id:number\n\n @authorize.read("ResourceOwner")\n @Column()\n email: string\n\n @Column()\n name: string\n}\n')),Object(l.b)("p",null,"Above configuration will make ",Object(l.b)("inlineCode",{parentName:"p"},"email")," filed only visible (in response body) by the owner of user, this behavior applied when the ",Object(l.b)("inlineCode",{parentName:"p"},"User")," entity used as child property. "),Object(l.b)("h4",{id:"entity-policy-restriction"},"Entity Policy Restriction"),Object(l.b)("p",null,"Since entity policy designed to secure resources by ID, its important to note that route authorization (defined with ",Object(l.b)("inlineCode",{parentName:"p"},"@authorize.route()")," or ",Object(l.b)("inlineCode",{parentName:"p"},"ActionBuilder.authorize()"),") and parameter authorization (",Object(l.b)("inlineCode",{parentName:"p"},"@authorize.write()"),") only works on route with entity ID specified."),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Route"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"POST /users")),Object(l.b)("td",{parentName:"tr",align:null},"Not allowed, since no ID specified")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"DELETE /users/{id}")),Object(l.b)("td",{parentName:"tr",align:null},"Allowed, Callback called with ID of User entity")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"POST /users/{id}/logs")),Object(l.b)("td",{parentName:"tr",align:null},"Allowed, Callback called with ID of User entity")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"DELETE /users/{id}/logs/{logId}")),Object(l.b)("td",{parentName:"tr",align:null},"Allowed, Callback called with ID of Log entity")))),Object(l.b)("p",null,"Important to note that response authorization (",Object(l.b)("inlineCode",{parentName:"p"},"@authorize.read()"),") has no restriction, its mean it will always work on all routes."),Object(l.b)("h2",{id:"request-hook"},"Request Hook"),Object(l.b)("p",null,"Request hook enables entity to have a method contains piece of code that will be executed during request. Request hook has some simple rule"),Object(l.b)("ul",null,Object(l.b)("li",{parentName:"ul"},"It executed before or after the entity saved into the database, use decorator ",Object(l.b)("inlineCode",{parentName:"li"},"@preSave()")," and ",Object(l.b)("inlineCode",{parentName:"li"},"@postSave()")),Object(l.b)("li",{parentName:"ul"},"It will be executed only on request with http method ",Object(l.b)("inlineCode",{parentName:"li"},"POST"),", ",Object(l.b)("inlineCode",{parentName:"li"},"PUT"),", ",Object(l.b)("inlineCode",{parentName:"li"},"PATCH"),". By default it will execute on those three http methods except specified on the parameter."),Object(l.b)("li",{parentName:"ul"},"It can be specified multiple request hooks on single entity"),Object(l.b)("li",{parentName:"ul"},"It can have parameter with parameter binding"),Object(l.b)("li",{parentName:"ul"},"It possible to bind ActionResult (execution result of the controller) on request hook ",Object(l.b)("inlineCode",{parentName:"li"},"@postSave()"))),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{20,26}","{20,26}":!0},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route, preSave } from "plumier"\nimport bcrypt from "bcrypt"\n\n@route.controller()\n@Entity()\nclass User {\n @PrimaryGeneratedColumn()\n id: number\n\n @Column()\n email: string\n\n @Column()\n name: string\n\n @Column()\n password: string\n\n @preSave()\n async hashPassword(){\n // executed before user saved\n this.password = await bcrypt.hash(data.password, 10)\n }\n\n @postSave()\n async sendConfirmationEmail(){\n // executed after user saved\n await mailer.sendTemplate("confirmation-email")\n }\n}\n')),Object(l.b)("p",null,"Above code will hash password before the entity saved into the database. Request hook has parameter binding feature, you can ",Object(l.b)("inlineCode",{parentName:"p"},"@bind")," any request part into the hook parameter, it works exactly like common ",Object(l.b)("a",{parentName:"p",href:"/controller#parameter-binding"},"Parameter Binding")," which also support name binding, model binding and decorator binding."),Object(l.b)("div",{className:"admonition admonition-info alert alert--info"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"14",height:"16",viewBox:"0 0 14 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"}))),"info")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"The ID of the current entity only accessible on ",Object(l.b)("inlineCode",{parentName:"p"},"@postSave")," using ",Object(l.b)("inlineCode",{parentName:"p"},"this.id"),", since on ",Object(l.b)("inlineCode",{parentName:"p"},"@postSave()")," the entity is not saved yet to database."))),Object(l.b)("div",{className:"admonition admonition-warning alert alert--danger"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"12",height:"16",viewBox:"0 0 12 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"}))),"warning")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"Keep in mind that entity used for ",Object(l.b)("inlineCode",{parentName:"p"},"@preSave()")," and ",Object(l.b)("inlineCode",{parentName:"p"},"@postSave()")," is different, means if you using state variable to share between ",Object(l.b)("inlineCode",{parentName:"p"},"@preSave()")," and ",Object(l.b)("inlineCode",{parentName:"p"},"@postSave()")," its may not working like expected."))),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route, preSave } from "plumier"\nimport bcrypt from "bcrypt"\n\n@route.controller()\n@Entity()\nclass User {\n \n /** other properties **/\n\n @preSave()\n async hook(@bind.ctx() ctx:Context){\n // ctx will contains context\n }\n\n @postSave()\n async postHook(@bind.ctx() ctx:Context){\n // ctx will contains context\n }\n}\n')),Object(l.b)("p",null,"Its possible to specify in which http method should the hook executed by specify http method on the request hook parameter"),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'import { Entity, PrimaryGeneratedColumn } from "typeorm"\nimport { route, preSave } from "plumier"\nimport bcrypt from "bcrypt"\n\n@route.controller()\n@Entity()\nclass User {\n \n /** other properties **/\n\n @preSave("put", "patch")\n async hook(){\n // this only executed on PUT and PATCH http method before entity saved\n }\n\n @postSave("put", "patch")\n async postHook(){\n // this only executed on PUT and PATCH http method after entity saved\n }\n}\n')),Object(l.b)("h2",{id:"use-custom-generic-controller"},"Use Custom Generic Controller"),Object(l.b)("p",null,"When the default generic controller doesn't match your need, you can provide your own custom generic controllers. For example the default generic controller for ",Object(l.b)("inlineCode",{parentName:"p"},"GET")," method doesn't contains count of the match records usually used for table pagination on UI side. You can override this function by provide a new generic controller inherited from your ORM/ODM helper: "),Object(l.b)("table",null,Object(l.b)("thead",{parentName:"table"},Object(l.b)("tr",{parentName:"thead"},Object(l.b)("th",{parentName:"tr",align:null},"Generic Controller"),Object(l.b)("th",{parentName:"tr",align:null},"Package"),Object(l.b)("th",{parentName:"tr",align:null},"Description"))),Object(l.b)("tbody",{parentName:"table"},Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"TypeORMControllerGeneric<T, TID>")),Object(l.b)("td",{parentName:"tr",align:null},"@plumier/typeorm"),Object(l.b)("td",{parentName:"tr",align:null},"TypeORM generic controller implementation")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"TypeORMOneToManyControllerGeneric<P, T, PID, TID>")),Object(l.b)("td",{parentName:"tr",align:null},"@plumier/typeorm"),Object(l.b)("td",{parentName:"tr",align:null},"TypeORM One To Many generic controller implementation")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"MongooseControllerGeneric<T, TID>")),Object(l.b)("td",{parentName:"tr",align:null},"@plumier/mongoose"),Object(l.b)("td",{parentName:"tr",align:null},"Mongoose generic controller implementation")),Object(l.b)("tr",{parentName:"tbody"},Object(l.b)("td",{parentName:"tr",align:null},Object(l.b)("inlineCode",{parentName:"td"},"MongooseOneToManyControllerGeneric<P, T, PID, TID>")),Object(l.b)("td",{parentName:"tr",align:null},"@plumier/mongoose"),Object(l.b)("td",{parentName:"tr",align:null},"Mongoose One To Many generic controller implementation")))),Object(l.b)("p",null,"First define the model represent the response schema returned by each controller using generic type like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'import {generic, type, noop} from "@plumier/reflect"\n\n@generic.template("T")\nexport class Response<T> {\n @type(["T"])\n data: T[]\n @noop()\n count: number\n}\n')),Object(l.b)("p",null,"Then create a new generic controller for both based on any of above generic controller like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript"},'import {TypeORMControllerGeneric, TypeORMOneToManyControllerGeneric} from "@plumier/typeorm"\n\n@generic.template("T", "TID")\n@generic.type("T", "TID")\nexport class CustomControllerGeneric<T, TID> extends TypeORMControllerGeneric<T, TID> {\n @type(Response, "T")\n async list(offset: number, limit: number, filter: FilterEntity<T>, select: string, order: string, ctx: Context) {\n const data = await super.list(offset, limit, filter, select, order, ctx)\n const count = await this.repo.count(filter)\n return { data, count } \n }\n}\n\n@generic.template("P", "T", "PID", "TID")\n@generic.type("P", "T", "PID", "TID")\nexport class CustomOneToManyControllerGeneric<P, T, PID, TID> extends TypeORMOneToManyControllerGeneric<P, T, PID, TID>{\n @type(Response, "T")\n async list(pid: PID, offset: number, limit: number, filter: FilterEntity<T>, select: string, order: string, ctx: Context) {\n const data = await super.list(pid, offset, limit, filter, select, order, ctx)\n const count = await this.repo.count(pid, filter)\n return { data, count } \n }\n}\n')),Object(l.b)("p",null,"Using above code, we simply call the ",Object(l.b)("inlineCode",{parentName:"p"},"super.list")," method and use the repository ",Object(l.b)("inlineCode",{parentName:"p"},"count")," method to create the response match the ",Object(l.b)("inlineCode",{parentName:"p"},"Response")," data structure."),Object(l.b)("p",null,"Note that we define the return type of the method as ",Object(l.b)("inlineCode",{parentName:"p"},'@type(Response, "T")'),", this means we specify that the ",Object(l.b)("inlineCode",{parentName:"p"},"Response.data")," property is of type of ",Object(l.b)("inlineCode",{parentName:"p"},"T")," of the generic controller."),Object(l.b)("p",null,"Next, we need to register above custom generic controller on the Plumier application like below "),Object(l.b)("pre",null,Object(l.b)("code",{parentName:"pre",className:"language-typescript",metastring:"{5}","{5}":!0},"new Plumier()\n .set(new WebApiFacility())\n .set(new TypeORMFacility())\n .set({\n genericController: [CustomControllerGeneric, CustomOneToManyControllerGeneric]\n })\n")),Object(l.b)("div",{className:"admonition admonition-note alert alert--secondary"},Object(l.b)("div",{parentName:"div",className:"admonition-heading"},Object(l.b)("h5",{parentName:"div"},Object(l.b)("span",{parentName:"h5",className:"admonition-icon"},Object(l.b)("svg",{parentName:"span",xmlns:"http://www.w3.org/2000/svg",width:"14",height:"16",viewBox:"0 0 14 16"},Object(l.b)("path",{parentName:"svg",fillRule:"evenodd",d:"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"}))),"note")),Object(l.b)("div",{parentName:"div",className:"admonition-content"},Object(l.b)("p",{parentName:"div"},"Make sure you register the controller under the ",Object(l.b)("inlineCode",{parentName:"p"},"TypeORMFacility")," or ",Object(l.b)("inlineCode",{parentName:"p"},"MongooseFacility")," to take effect. "))))}p.isMDXComponent=!0}}]);