diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..07d0ad5b4 --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + + + + + + + + | Coalesce + + + + +

404

That's a Four-Oh-Four.
Take me home
+ + + diff --git a/assets/404.html.4f7b27d6.js b/assets/404.html.4f7b27d6.js new file mode 100644 index 000000000..c75c79838 --- /dev/null +++ b/assets/404.html.4f7b27d6.js @@ -0,0 +1 @@ +import{_ as e,y as t,z as _}from"./framework.fe9a73df.js";const c={};function r(n,o){return t(),_("div")}const a=e(c,[["render",r],["__file","404.html.vue"]]);export{a as default}; diff --git a/assets/404.html.98e87f94.js b/assets/404.html.98e87f94.js new file mode 100644 index 000000000..2a1a1ca6f --- /dev/null +++ b/assets/404.html.98e87f94.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-3706649a","path":"/404.html","title":"","lang":"en-US","frontmatter":{"layout":"NotFound"},"excerpt":"","headers":[],"git":{},"filePathRelative":null}');export{t as data}; diff --git a/assets/CodeTabs.14b1e322.js b/assets/CodeTabs.14b1e322.js new file mode 100644 index 000000000..e869ceba0 --- /dev/null +++ b/assets/CodeTabs.14b1e322.js @@ -0,0 +1 @@ +import{_,b as p,a9 as f,y as s,z as i,X as l,U as c,V as g,D as r,C as h,a3 as L,a4 as m,A as b}from"./framework.fe9a73df.js";const n={groups:{default:{vue:"Vue",knockout:"Knockout"},vue:{options:"Options API",setup:"Composition API"},"vue-bundler":{"vue-cli":"Vue CLI",vite:"Vite"}}},u=p({}),v={props:{name:{type:String,default:"default"},isolated:{type:Boolean,default:!1},languages:{type:Object,required:!1}},data(){return{selectedLanguage:null,actualLanguages:{}}},computed:{localStorageKey(){return`vuepress-plugin-code-switcher@${this.name}`}},methods:{switchLanguage(a){if(this.isolated)return this.selectedLanguage=a;typeof localStorage<"u"&&localStorage.setItem(this.localStorageKey,a),u[this.name]=a},setConfiguredDefaultLanguages(){this.languages?this.actualLanguages=this.languages:n&&n.groups&&n.groups[this.name]&&(this.actualLanguages=n.groups[this.name]),this.selectedLanguage=Object.keys(this.actualLanguages)[0],u[this.name]||(u[this.name]=this.selectedLanguage)}},created(){if(!this.isolated){if(this.setConfiguredDefaultLanguages(),!this.actualLanguages)throw new Error('You must specify either the "languages" prop or use the "groups" option when configuring the plugin.');if(typeof localStorage<"u"){let a=localStorage.getItem(this.localStorageKey);a&&Object.keys(this.actualLanguages).indexOf(a)!==-1&&(this.selectedLanguage=a)}f(()=>this.selectedLanguage=u[this.name])}}},y={class:"code-tabs"},k={class:"code-tabs__nav"},C={class:"code-tabs__ul"},S=["aria-pressed","aria-expanded","onClick"],w=["aria-selected"];function x(a,D,I,O,t,d){return s(),i("div",y,[l("div",k,[l("ul",C,[(s(!0),i(c,null,g(t.actualLanguages,(o,e)=>(s(),i("li",{key:e,class:"code-tabs__li"},[l("button",{class:r(["code-tabs__nav-tab",{"code-tabs__nav-tab-active":t.selectedLanguage===e}]),"aria-pressed":t.selectedLanguage===e,"aria-expanded":t.selectedLanguage===e,onClick:V=>d.switchLanguage(e)},h(o),11,S)]))),128))])]),(s(!0),i(c,null,g(t.actualLanguages,(o,e)=>L((s(),i("div",{key:e,class:r(["code-tabs-item",{"code-tabs-item__active":t.selectedLanguage===e}]),"aria-selected":t.selectedLanguage===e},[b(a.$slots,e)],10,w)),[[m,e===t.selectedLanguage]])),128))])}const K=_(v,[["render",x],["__file","CodeTabs.vue"]]);export{K as default}; diff --git a/assets/Prop.6195109b.js b/assets/Prop.6195109b.js new file mode 100644 index 000000000..ca01082c5 --- /dev/null +++ b/assets/Prop.6195109b.js @@ -0,0 +1,7 @@ +import{_ as f}from"./app.09d0ccf5.js";import{c as u,_ as g,y as h,z as l,W as m,X as d,B as c,C as _}from"./framework.fe9a73df.js";var a=new Map;async function y(t){const e=await f(()=>import("./index.esm.dea45d94.js"),[]);return e.setCDN("https://unpkg.com/shiki/"),{highlighter:await e.getHighlighter({theme:"dark-plus",langs:[t]}),shiki:e}}function b(t){if(a.has(t))return a.get(t);const e=y(t);return a.set(t,e),e}const k=u({props:{def:{type:String,required:!0},lang:{type:String,default:"c#"},ctor:{type:[Number,String],default:null},noClass:{type:Boolean,default:!1},id:{type:String,default:null},idPrefix:{type:String,default:"member"}},data(){return{idAttr:"",html:""}},async serverPrefetch(){await this.renderHtml()},beforeMount(){var t,e;this.html=(t=this.$el)==null?void 0:t.innerHTML,this.idAttr=(e=this.$el)==null?void 0:e.id,this.html||this.$watch("def",()=>this.renderHtml(),{immediate:!0})},methods:{async renderHtml(){var t=null;this.ctor&&(t="// Also settable via constructor parameter #"+this.ctor,(this.lang=="c#"||this.lang=="csharp")&&!this.def.match(/\bset\b/)&&(t="// ONLY settable via constructor parameter #"+this.ctor));const e=(this.noClass?"":"public class x {")+(t?` +`+t:"")+` +`+this.def+(this.noClass?"":` +}`);if(this.id)this.idAttr=this.id;else if(this.lang=="ts"){const i=/(?:(?:readonly|public|static|protected|private|abstract|export) )*((?:namespace )?[\w$]+)/.exec(this.def);this.idAttr=i?this.idPrefix+"-"+i[1]:null}else if(this.lang=="c#"){const i=/ (\w+)(?:<|\(| \{|$)/.exec(this.def);this.idAttr=i?this.idPrefix+"-"+i[1]:null}if(!this.idAttr)throw new Error("Unable to compute id for Prop "+this.def);this.idAttr=this.idAttr.toLowerCase().replace(/[ &<>"']/g,"-").replace(/\$/g,"_");const{highlighter:s,shiki:n}=await b(this.lang),r=s.codeToThemedTokens(e,this.lang);this.noClass||(r.shift(),r.pop());const o=s.getTheme();let p=n.renderToHtml(r,{fg:o.fg,bg:o.bg});this.html=`${p}`}}});const A=["innerHTML","id"],v=["id"],$={class:"shiki",style:{"background-color":"#1E1E1E","line-height":"1.18","padding-top":"1px","padding-bottom":"4px"}};function w(t,e,s,n,r,o){return t.html?(h(),l("h4",{key:0,innerHTML:t.html,class:"code-prop",id:t.idAttr},null,8,A)):(h(),l("h4",{key:1,class:"code-prop",id:t.idAttr},[m(" Temporary uncolored content that matches the result as best as possible to avoid FOUC "),d("pre",$,[c(" "),d("code",null,` + `+_(t.def)+` + `,1),c(` + `)])],8,v))}const P=g(k,[["render",w],["__file","Prop.vue"]]);export{P as default}; diff --git a/assets/api-clients.html.3a38f66c.js b/assets/api-clients.html.3a38f66c.js new file mode 100644 index 000000000..a68eee124 --- /dev/null +++ b/assets/api-clients.html.3a38f66c.js @@ -0,0 +1,58 @@ +import{_ as d,y as u,z as h,W as p,X as s,B as e,Q as n,$ as a,a5 as i,P as c}from"./framework.fe9a73df.js";const D={},y=s("h1",{id:"vue-api-client-layer",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#vue-api-client-layer","aria-hidden":"true"},"#"),e(" Vue API Client Layer")],-1),m=s("code",null,"api-clients.g.ts",-1),f={href:"https://github.com/axios/axios",target:"_blank",rel:"noopener noreferrer"},v=s("code",null,"coalesce-vue",-1),C=s("code",null,"AxiosClient",-1),g={href:"https://axios-http.com/docs/interceptors",target:"_blank",rel:"noopener noreferrer"},b={href:"https://axios-http.com/docs/config_defaults",target:"_blank",rel:"noopener noreferrer"},_={href:"https://github.com/IntelliTect/Coalesce/blob/dev/src/coalesce-vue/src/api-client.ts",target:"_blank",rel:"noopener noreferrer"},w={class:"table-of-contents"},A=i('

Concepts

API Client

A class, generated for each controller-backed type in your data model as <ModelName>ApiClient and exported from api-clients.g.ts containing one method for each API endpoint.

Each method on the API client takes in the regular parameters of the method as you would expect, as well as an optional AxiosRequestConfig parameter at the end that can be used to provide additional configuration for the single request, if needed.

',4),E=s("code",null,"get",-1),k=s("code",null,"list",-1),q=s("code",null,"count",-1),F=s("code",null,"save",-1),I=s("code",null,"delete",-1),x=s("code",null,"parameters",-1),T=s("code",null,"Promise>",-1),P=s("code",null,"TApiResult",-1),R=s("code",null,"ItemResult",-1),S=s("code",null,"ItemResult",-1),L=s("code",null,"ListResult",-1),j=s("code",null,"AxiosResponse",-1),B={href:"https://axios-http.com/docs/res_schema",target:"_blank",rel:"noopener noreferrer"},W=s("code",null,"TApiResult",-1),M=s("code",null,"data",-1),O=s("code",null,"headers",-1),V=s("code",null,"T",-1),N=s("h3",{id:"api-callers-api-states",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#api-callers-api-states","aria-hidden":"true"},"#"),e(" API Callers/API States")],-1),U=s("p",null,[e("A stateful function for invoking an API endpoint, created with the "),s("code",null,"$makeCaller"),e(" function on an API Client. API Callers provide a wide array of functionality that is useful for working with API endpoints that are utilized by a user interface.")],-1),H=s("code",null,"coalesce-vue",-1),$=i('

API Callers

API Callers (typed with the name ApiState in coalesce-vue, sometimes also referred to as "loaders" or "invokers") are stateful functions for invoking an API endpoint, created with the $makeCaller function on an API Client. A summary of features:

Endpoint Invocation

Each API Caller is itself a function, so it can be invoked to trigger an API request to the server.

State management

API Callers contain properties about the last request made, including things like wasSuccessful, isLoading, result, and more.

Concurrency Management

Using setConcurrency(mode), you can configure how each individual caller handles what happens when multiple requests are made simultaneously

Argument Binding

',9),z=s("code",null,"args",-1),K=s("code",null,".invokeWithArgs()",-1),G=s("h3",{id:"creating-and-invoking-an-api-caller",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#creating-and-invoking-an-api-caller","aria-hidden":"true"},"#"),e(" Creating and Invoking an API Caller")],-1),Q=s("p",null,[e("API Callers can be created with the "),s("code",null,"$makeCaller"),e(" method of an API Client. The way in which it was created affects how it is invoked, as the parameters that the caller accepts are defined when it is created.")],-1),X={class:"custom-container tip"},J=s("p",{class:"custom-container-title"},"TIP",-1),Y=i(`

Some examples:

// Preamble for all the examples below:
+import { PersonApiClient } from '@/api-clients.g';
+const client = new PersonApiClient;
+

A caller that takes no additional parameters:

const caller = client.$makeCaller(
+    "item", 
+    c => c.namesStartingWith("A")
+);
+
+await caller();
+console.log(caller.result)
+

A caller that takes custom parameters:

const caller = client.$makeCaller(
+    methods => methods.namesStartingWith, 
+    (c, str: string) => c.namesStartingWith(str)
+);
+
+await caller("Rob");
+console.log(caller.result)
+
`,6),Z=i(`
const caller = client.$makeCaller("item", 
+    // The parameter-based version is always required, even if it won't be used.
+    (c, str: string) => c.namesStartingWith(str),
+    // A function which creates a blank instance of the args object.
+    // All props should be initialized (i.e. not undefined) to work with Vue's reactivity.
+    () => ({str: null as string | null, }),
+    // The function that accepts the args object and uses it:
+    (c, args) => c.namesStartingWith(args.str)
+);
+
+caller.args.str = "Su";
+await caller.invokeWithArgs();
+console.log(caller.result)
+

A caller that performs multiple async operations:

const deleteFirstNameStartingWith = client.$makeCaller(
+    "item",
+    async (c, str: string) => {
+        const namesResult = await c.namesStartingWith(str)
+        return await c.deletePersonByName(namesResult.data.object[0])
+    }
+);
+
+await caller("Rob");
+console.log(caller.result)
+

The first parameter, resultType, can either be one of "item" or "list", indicating whether the method returns a ItemResult or ListResult (examples #1 and #3 above). It can also be a function which accepts the set of method metadata for the API Client and which returns the specific method metadata (example #2 above), or it can be a direct reference to a specific method metadata object.

Properties

The following state properties can be found on API Caller instances. These properties are useful for binding to in a user interface to display errors, results, or indicators of progress.

All Callers

`,7),ee=s("p",null,"True if there is currently a request pending for the API Caller.",-1),se=s("p",null,"A boolean indicating if the last request made was successful, or null if either no request has been made yet, or if a request has been made but has not yet completed.",-1),ne=s("p",null,"An error message from the last request, if any. Will be set to null upon successful completion of a request.",-1),ae=s("p",null,[e("True if "),s("code",null,"result"),e(" is non-null. This prop is useful in performance-critical scenarios where checking "),s("code",null,"result"),e(" directly will cause an overabundance of re-renders in high-churn scenarios.")],-1),oe=s("p",null,[e("Holds an object for the arguments of the function, and will be used if the caller is invoked with its "),s("code",null,"invokeWithArgs()"),e(" method. Useful for binding the arguments of a caller to inputs in a user interface.")],-1),le=s("p",null,"Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.",-1),te=s("p",null,[e("Returns the URL for the method's HTTP endpoint. Any parameters are sourced from the "),s("code",null,"args"),e(" object. Useful for binding file-returning HTTP GET methods directly to "),s("code",null,"image"),e(" or "),s("code",null,"video"),e(" HTML elements.")],-1),re=s("p",null,"Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.",-1),ie=s("h4",{id:"itemresult-based-callers",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#itemresult-based-callers","aria-hidden":"true"},"#"),e(" ItemResult-based Callers")],-1),ce=s("p",null,"The principal data returned by the previous request. Will be set to null if the last response received returned no data (e.g. if the response was an error response)",-1),pe=s("h4",{id:"listresult-based-callers",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#listresult-based-callers","aria-hidden":"true"},"#"),e(" ListResult-based Callers")],-1),de=s("p",null,"The principal data returned by the previous request. Will be set to null if the last response received returned no data (e.g. if the response was an error response).",-1),ue=s("p",null,"Properties which contain the pagination information returned by the previous request.",-1),he=s("h3",{id:"concurrency-mode",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#concurrency-mode","aria-hidden":"true"},"#"),e(" Concurrency Mode")],-1),De=i('

API callers have a setConcurrency method that allows you to customize how they behave when additional invocations are performed when there is already a request pending. There are four options available, with "disallow" being the default:

"disallow"

The default behavior - simply throws an error for any secondary invocations.

Note

Having "disallow" as the default prevents the unexpected behavior that can happen in a number of ways with the other modes:

  • For requests that are performing data-mutating actions on the server, all other concurrency modes could lead to an unexpected end state of the data due to requests either being abandoned, cancelled, or potentially happening out-of-order.
  • Throwing errors for multiple concurrent requests quickly surfaces issues during development where concurrent requests are not being correctly guarded against in a user interface - e.g. not disabling a "Save" or "Submit" button while the request is pending, which would otherwise lead to double-posts.
"debounce"

When a secondary invocation is performed, enqueue it after the current pending invocation completes.

If additional invocations are performed while there is already an invocation enqueued and waiting, the already-enqueued invocation is abandoned and replaced by the most recent invocation attempt. The promise of the abandoned invocation will be resolved with undefined (it is NOT rejected).

"cancel"

When a secondary invocation is performed, cancel the current pending invocation.

',9),ye={href:"https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httpcontext.requestaborted?view=aspnetcore-3.1",target:"_blank",rel:"noopener noreferrer"},me=s("code",null,"undefined",-1),fe=i('
"allow"

When a secondary invocation is performed, always continue normally, sending the request to the server.

The state of the properties on the caller at any time will reflect the most recent response received from the server, which is never guaranteed to correlate with the most recent request made to the server - that is, requests are not guaranteed to complete in the order they were made. In particular, the isLoading property will be false after the first response comes back, even if the second response has not yet been received.

WARNING

For the reasons outlined above, it is generally not recommended to use "allow" unless you fully understand the drawbacks. This mode mirrors the legacy behavior of the Knockout stack for Coalesce.

Response Caching

Response caching on API Callers is a feature that will save API responses to persistent storage (sessionStorage or localStorage). The next time a matching request is made, the result property of the API Caller will be populated with that saved response, allowing for a faster time to interactivity and reduced repaints and shifting of elements as initial data loads after a page navigation. It does not prevent any HTTP requests from being made, and does not affect the Promise returned from invoke or invokeWithArgs.

Common use cases include:

  • Site-wide status or alert messages
  • Server-provided configuration
  • Dashboard data, like statistics or graphs

When a cached response is loaded, result is populated with that response's data, wasSuccessful and hasResult are set to true, and onFulfilled callbacks are invoked.

',9),ve=s("code",null,"false",-1),Ce=i(`
export type ResponseCachingConfiguration = {
+  /** Function that will determine the cache key used for a particular request.
+   * Return a falsy value to prevent caching. The default key is the request URL.
+   */
+  key?: (
+    req: AxiosRequestConfig,
+    defaultKey: string
+  ) => string | null | undefined;
+
+  /** The maximum age of a cached response. If null, the entry will not expire. Default 1 hour.
+   *
+   * The smallest of the current configured max age and the max age that was set at the time of the cached response is used. */
+  maxAgeSeconds?: number | null;
+
+  /** The Storage (default \`sessionStorage\`) that will hold cached responses. */
+  storage?: Storage;
+};
+

Other Methods

API Callers have a few other methods available as well:

`,3),ge=s("p",null,[e("Manually cancel the current request. The promise of the cancelled invocation will be resolved with "),s("code",null,"undefined"),e(" (it is NOT rejected). If using concurrency mode "),s("code",null,'"allow"'),e(", only the most recent invocation is cancelled.")],-1),be=s("p",null,[e("Add a callback to the caller to be invoked when a success response is received from the server. If a promise is returned, this promise will be awaited and will delay the setting of the "),s("code",null,"isLoading"),e(" prop to "),s("code",null,"false"),e(" until it completes.")],-1),_e=s("p",null,[e("Add a callback to the caller to be invoked when a failure response is received from the server. If a promise is returned, this promise will be awaited and will delay the setting of the "),s("code",null,"isLoading"),e(" prop to "),s("code",null,"false"),e(" until it completes.")],-1),we=s("p",null,[e("The invoke function is a reference from the caller to itself. In other words, "),s("code",null,"caller.invoke === caller"),e(". This exists to mirror the syntax of the Knockout generated method classes.")],-1),Ae=s("p",null,[e("If called a parameter, that parameter will be used as the args object. Otherwise, "),s("code",null,"caller.args"),e(" will be used.")],-1),Ee=s("p",null,"Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.",-1),ke={href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noopener noreferrer"},qe=s("code",null,"result",-1),Fe=s("p",null,[e("Accepts a "),s("code",null,"Vue"),e(" instance in order to manage the lifecycle of the URL, since object URLs must be manually released to avoid memory leaks. When the provided Vue component is destroyed, the object URL will be destroyed. If called inside the component template, the Vue instance can be acquired automatically.")],-1),Ie=s("p",null,"Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.",-1);function xe(Te,Pe){const l=c("RouterLink"),r=c("ExternalLinkIcon"),t=c("router-link"),o=c("Prop");return u(),h("div",null,[y,p(" MARKER:summary "),s("p",null,[e("The API client layer, generated as "),m,e(", exports a class for each API controller that was generated for your data model. These classes are stateless and provide one method for each API endpoint. This includes both the standard set of endpoints created for "),n(l,{to:"/modeling/model-types/entities.html"},{default:a(()=>[e("Entity Models")]),_:1}),e(" and "),n(l,{to:"/modeling/model-types/dtos.html"},{default:a(()=>[e("Custom DTOs")]),_:1}),e(", as well as any custom "),n(l,{to:"/modeling/model-components/methods.html"},{default:a(()=>[e("Methods")]),_:1}),e(" on the aforementioned types, as well as any methods on your "),n(l,{to:"/modeling/model-types/services.html"},{default:a(()=>[e("Services")]),_:1}),e(".")]),p(" MARKER:summary-end "),s("p",null,[e("The API clients provided by Coalesce are based on "),s("a",f,[e("axios"),n(r)]),e(". All API clients used a shared axios instance, exported from "),v,e(" as "),C,e(". This instance can be used to configure all HTTP requests made by Coalesce, including things like attaching "),s("a",g,[e("interceptors"),n(r)]),e(" to modify the requests being made, or configuring "),s("a",b,[e("defaults"),n(r)]),e(".")]),s("p",null,[e("As with all the layers, the "),s("a",_,[e("source code of coalesce-vue"),n(r)]),e(" is also a great supplement to this documentation.")]),s("nav",w,[s("ul",null,[s("li",null,[n(t,{to:"#concepts"},{default:a(()=>[e("Concepts")]),_:1}),s("ul",null,[s("li",null,[n(t,{to:"#api-client"},{default:a(()=>[e("API Client")]),_:1})]),s("li",null,[n(t,{to:"#api-callers-api-states"},{default:a(()=>[e("API Callers/API States")]),_:1})])])]),s("li",null,[n(t,{to:"#api-callers"},{default:a(()=>[e("API Callers")]),_:1}),s("ul",null,[s("li",null,[n(t,{to:"#creating-and-invoking-an-api-caller"},{default:a(()=>[e("Creating and Invoking an API Caller")]),_:1})]),s("li",null,[n(t,{to:"#properties"},{default:a(()=>[e("Properties")]),_:1})]),s("li",null,[n(t,{to:"#concurrency-mode"},{default:a(()=>[e("Concurrency Mode")]),_:1})]),s("li",null,[n(t,{to:"#response-caching"},{default:a(()=>[e("Response Caching")]),_:1})]),s("li",null,[n(t,{to:"#other-methods"},{default:a(()=>[e("Other Methods")]),_:1})])])])])]),A,s("p",null,[e("For the methods that correspond to the standard set of CRUD endpoints that Coalesce provides ("),E,e(", "),k,e(", "),q,e(", "),F,e(", "),I,e("), an additional parameter "),x,e(" is available that accepts the set of "),n(l,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:a(()=>[e("Standard Parameters")]),_:1}),e(" appropriate for the endpoint.")]),s("p",null,[e("Each method returns a "),T,e(" where "),P,e(" is either "),R,e(", "),S,e(", or "),L,e(", depending on the return type of the API endpoint. "),j,e(" is the "),s("a",B,[e("response object from axios"),n(r)]),e(", containing the "),W,e(" in its "),M,e(" property, as well as other properties like "),O,e(". The returned type "),V,e(" is automatically converted into valid "),n(l,{to:"/stacks/vue/layers/models.html"},{default:a(()=>[e("Model implementations")]),_:1}),e(" for you.")]),N,U,s("p",null,[e("Because they are such an integral part of the overall picture of "),H,e(", they have "),n(l,{to:"/stacks/vue/layers/api-clients.html"},{default:a(()=>[e("their own section below")]),_:1}),e(" where they are explained in much greater detail.")]),$,s("p",null,[e("API Callers can be created so that they have an "),z,e(" object that can be bound to, using "),K,e(" to make a request using those arguments as the API endpoint's parameters. The API Callers created for the "),n(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[e("ViewModel Layer")]),_:1}),e(" are all created this way.")]),G,Q,s("div",X,[J,s("p",null,[e("During typical development, it is unlikely that you'll need to make a custom API Caller - the ones created for you on the generated "),n(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[e("ViewModel Layer")]),_:1}),e(" will usually suffice. However, creating your own can allow for some more advanced functionality.")])]),Y,s("p",null,[e("A caller that has an args object that can be bound to. This is how the generated API Callers in the "),n(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[e("ViewModel Layer")]),_:1}),e(" are created:")]),Z,n(o,{def:"isLoading: boolean",lang:"ts"}),ee,n(o,{def:"wasSuccessful: boolean | null",lang:"ts"}),se,n(o,{def:"message: string | null",lang:"ts"}),ne,n(o,{def:"hasResult: boolean",lang:"ts"}),ae,n(o,{def:"args: {}",lang:"ts"}),oe,le,n(o,{def:"get url(): string",lang:"ts"}),te,re,ie,n(o,{def:"result: T | null",lang:"ts",id:"member-result-item"}),ce,n(o,{def:"validationIssues: ValidationIssue[] | null",lang:"ts"}),s("p",null,[e("Any validation issues returned by the previous request. This is never populated automatically by Coalesce, and is therefore is only used if you have written custom code to populate it in your "),n(l,{to:"/modeling/model-components/behaviors.html"},{default:a(()=>[e("Behaviors")]),_:1}),e(" or "),n(l,{to:"/modeling/model-components/methods.html"},{default:a(()=>[e("Methods")]),_:1}),e(".")]),pe,n(o,{def:"result: Array | null",lang:"ts",id:"member-result-list"}),de,n(o,{def:"page, pageSize, pageCount, totalCount: number | null",lang:"ts",id:"members-pagination"}),ue,he,n(o,{def:"setConcurrency(mode: 'disallow' | 'debounce' | 'cancel' | 'allow')",lang:"ts"}),De,s("p",null,[e("This completely aborts the request, propagating all the way back to the server where cancellation can be observed with "),s("a",ye,[e("HttpContext.RequestAborted"),n(r)]),e(". The promise of the cancelled invocation will be resolved with "),me,e(" (it is NOT rejected).")]),fe,n(o,{def:"useResponseCaching(configuration?: ResponseCachingConfiguration | false)",lang:"ts"}),s("p",null,[e("Enables response caching on the API Caller. Only "),n(l,{to:"/modeling/model-components/attributes/controller-action.html"},{default:a(()=>[e("HTTP GET methods")]),_:1}),e(" are supported, and "),n(l,{to:"/modeling/model-components/methods.html#file-downloads"},{default:a(()=>[e("file-returning methods")]),_:1}),e(" are not supported. Call with "),ve,e(" to disable caching after it was previously enabled. The available options are as follows:")]),Ce,n(o,{def:"cancel(): void",lang:"ts"}),ge,n(o,{def:"onFulfilled((state: TInvoker) => void | Promise): void",lang:"ts"}),be,n(o,{def:"onRejected((state: TInvoker) => void | Promise): void",lang:"ts"}),_e,n(o,{def:"invoke(...args: TArgs)",lang:"ts"}),we,n(o,{def:"invokeWithArgs(args?: {})",lang:"ts"}),Ae,Ee,n(o,{def:"getResultObjectUrl(vue?: Vue): string | undefined",lang:"ts"}),s("p",null,[e("If the method returns a file, this method will return an "),s("a",ke,[e("Object URL"),n(r)]),e(" representing the value of the "),qe,e(" prop.")]),Fe,Ie])}const Se=d(D,[["render",xe],["__file","api-clients.html.vue"]]);export{Se as default}; diff --git a/assets/api-clients.html.d06fd0bd.js b/assets/api-clients.html.d06fd0bd.js new file mode 100644 index 000000000..84314807d --- /dev/null +++ b/assets/api-clients.html.d06fd0bd.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4a7e6fdd","path":"/stacks/vue/layers/api-clients.html","title":"Vue API Client Layer","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Concepts","slug":"concepts","link":"#concepts","children":[{"level":3,"title":"API Client","slug":"api-client","link":"#api-client","children":[]},{"level":3,"title":"API Callers/API States","slug":"api-callers-api-states","link":"#api-callers-api-states","children":[]}]},{"level":2,"title":"API Callers","slug":"api-callers","link":"#api-callers","children":[{"level":3,"title":"Creating and Invoking an API Caller","slug":"creating-and-invoking-an-api-caller","link":"#creating-and-invoking-an-api-caller","children":[]},{"level":3,"title":"Properties","slug":"properties","link":"#properties","children":[]},{"level":3,"title":"Concurrency Mode","slug":"concurrency-mode","link":"#concurrency-mode","children":[]},{"level":3,"title":"Response Caching","slug":"response-caching","link":"#response-caching","children":[]},{"level":3,"title":"Other Methods","slug":"other-methods","link":"#other-methods","children":[]}]}],"git":{"updatedTime":1678748411000},"filePathRelative":"stacks/vue/layers/api-clients.md"}');export{e as data}; diff --git a/assets/app.09d0ccf5.js b/assets/app.09d0ccf5.js new file mode 100644 index 000000000..d1606fa98 --- /dev/null +++ b/assets/app.09d0ccf5.js @@ -0,0 +1 @@ +import{d as o,r as f,a as S,b as C,c as L,i as p,e as x,f as $,g as H,o as D,h as d,j as h,k as g,l as M,m as j,n as F,R as N,p as B,q as U,S as G,u as q,w as K,s as z,t as W,v as J}from"./framework.fe9a73df.js";const Y="modulepreload",Z=function(e){return"/Coalesce/"+e},T={},t=function(i,s,a){if(!s||s.length===0)return i();const c=document.getElementsByTagName("link");return Promise.all(s.map(n=>{if(n=Z(n),n in T)return;T[n]=!0;const r=n.endsWith(".css"),l=r?'[rel="stylesheet"]':"";if(!!a)for(let E=c.length-1;E>=0;E--){const y=c[E];if(y.href===n&&(!r||y.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${n}"]${l}`))return;const m=document.createElement("link");if(m.rel=r?"stylesheet":Y,r||(m.as="script",m.crossOrigin=""),m.href=n,document.head.appendChild(m),r)return new Promise((E,y)=>{m.addEventListener("load",E),m.addEventListener("error",()=>y(new Error(`Unable to preload CSS for ${n}`)))})})).then(()=>i())},Q={"v-a7d34384":()=>t(()=>import("./history.html.f58a3d69.js"),[]).then(({data:e})=>e),"v-8daa1a0e":()=>t(()=>import("./index.html.6f4afde9.js"),[]).then(({data:e})=>e),"v-5ee46ccb":()=>t(()=>import("./include-tree.html.78a40435.js"),[]).then(({data:e})=>e),"v-5d077123":()=>t(()=>import("./includes.html.5136332d.js"),[]).then(({data:e})=>e),"v-6db94f86":()=>t(()=>import("./coalesce-json.html.8fc0bc73.js"),[]).then(({data:e})=>e),"v-74ba1c67":()=>t(()=>import("./security.html.4972855d.js"),[]).then(({data:e})=>e),"v-48c2b024":()=>t(()=>import("./startup.html.05150e47.js"),[]).then(({data:e})=>e),"v-ad0e9b2c":()=>t(()=>import("./attributes.html.0e4b72e3.js"),[]).then(({data:e})=>e),"v-ada1a0e0":()=>t(()=>import("./behaviors.html.392a1226.js"),[]).then(({data:e})=>e),"v-2960e828":()=>t(()=>import("./data-sources.html.6cd385d1.js"),[]).then(({data:e})=>e),"v-0f1d24df":()=>t(()=>import("./methods.html.cb5a7d01.js"),[]).then(({data:e})=>e),"v-57b8d80e":()=>t(()=>import("./properties.html.bcebe6c1.js"),[]).then(({data:e})=>e),"v-d0002c24":()=>t(()=>import("./dtos.html.c910d3c7.js"),[]).then(({data:e})=>e),"v-5f0df441":()=>t(()=>import("./entities.html.1679e7fa.js"),[]).then(({data:e})=>e),"v-3d8f170b":()=>t(()=>import("./external-types.html.67a4ae13.js"),[]).then(({data:e})=>e),"v-d699f478":()=>t(()=>import("./services.html.c1892de9.js"),[]).then(({data:e})=>e),"v-94b3a3fa":()=>t(()=>import("./dtos.html.01f136aa.js"),[]).then(({data:e})=>e),"v-7231735f":()=>t(()=>import("./generation.html.c0787daf.js"),[]).then(({data:e})=>e),"v-ba2d632e":()=>t(()=>import("./getting-started-modeling.html.c1e6e2c4.js"),[]).then(({data:e})=>e),"v-76e9aad4":()=>t(()=>import("./external-view-model.html.6ceaf9c7.js"),[]).then(({data:e})=>e),"v-32435eb2":()=>t(()=>import("./list-view-model.html.80510cd0.js"),[]).then(({data:e})=>e),"v-0c9f605a":()=>t(()=>import("./view-model.html.b9d16d3b.js"),[]).then(({data:e})=>e),"v-83dc9a7e":()=>t(()=>import("./getting-started.html.72eb3d61.js"),[]).then(({data:e})=>e),"v-930615c0":()=>t(()=>import("./overview.html.9101164c.js"),[]).then(({data:e})=>e),"v-91a9c356":()=>t(()=>import("./getting-started.html.435781ec.js"),[]).then(({data:e})=>e),"v-4966afe8":()=>t(()=>import("./overview.html.9d82da2f.js"),[]).then(({data:e})=>e),"v-6ffb6087":()=>t(()=>import("./vue2-to-vue3.html.6bf93dcd.js"),[]).then(({data:e})=>e),"v-f401e784":()=>t(()=>import("./client-validation.html.64bb4309.js"),[]).then(({data:e})=>e),"v-19f90752":()=>t(()=>import("./coalesce.html.2758c118.js"),[]).then(({data:e})=>e),"v-3903d1f2":()=>t(()=>import("./controller-action.html.0ba1d19c.js"),[]).then(({data:e})=>e),"v-10bb147d":()=>t(()=>import("./controller.html.b4140e47.js"),[]).then(({data:e})=>e),"v-6b92cba8":()=>t(()=>import("./create-controller.html.76caf7a4.js"),[]).then(({data:e})=>e),"v-2ddfa8c0":()=>t(()=>import("./date-type.html.b681aadc.js"),[]).then(({data:e})=>e),"v-28165697":()=>t(()=>import("./default-order-by.html.638f5e51.js"),[]).then(({data:e})=>e),"v-ee3c31e8":()=>t(()=>import("./dto-includes-excludes.html.5a9182f5.js"),[]).then(({data:e})=>e),"v-1e992cc4":()=>t(()=>import("./execute.html.29138de0.js"),[]).then(({data:e})=>e),"v-f75859a2":()=>t(()=>import("./hidden.html.383280bc.js"),[]).then(({data:e})=>e),"v-60941fa8":()=>t(()=>import("./inject.html.859d9f07.js"),[]).then(({data:e})=>e),"v-440fbe82":()=>t(()=>import("./internal-use.html.bddc07d7.js"),[]).then(({data:e})=>e),"v-05ce079d":()=>t(()=>import("./list-text.html.7340eedf.js"),[]).then(({data:e})=>e),"v-e0635f52":()=>t(()=>import("./load-from-data-source.html.ec39c8f4.js"),[]).then(({data:e})=>e),"v-d6277114":()=>t(()=>import("./many-to-many.html.41f6bed5.js"),[]).then(({data:e})=>e),"v-d3094c1e":()=>t(()=>import("./search.html.6125f4c3.js"),[]).then(({data:e})=>e),"v-492e3baa":()=>t(()=>import("./security-attribute.html.6ed22e0c.js"),[]).then(({data:e})=>e),"v-3a375f50":()=>t(()=>import("./select-filter.html.a53f4884.js"),[]).then(({data:e})=>e),"v-1a8eae00":()=>t(()=>import("./typescript-partial.html.d72d268f.js"),[]).then(({data:e})=>e),"v-024b84b2":()=>t(()=>import("./bindings.html.5a16a120.js"),[]).then(({data:e})=>e),"v-1edd1d5a":()=>t(()=>import("./external-view-model.html.280bee2c.js"),[]).then(({data:e})=>e),"v-30cbf62d":()=>t(()=>import("./list-view-model.html.c0e8f6d8.js"),[]).then(({data:e})=>e),"v-0f867a4b":()=>t(()=>import("./methods.html.f2136319.js"),[]).then(({data:e})=>e),"v-275d16cf":()=>t(()=>import("./model-config.html.82e5d884.js"),[]).then(({data:e})=>e),"v-0f1441d8":()=>t(()=>import("./view-model.html.9f81154a.js"),[]).then(({data:e})=>e),"v-33aa6bf4":()=>t(()=>import("./overview.html.36fc7252.js"),[]).then(({data:e})=>e),"v-4a7e6fdd":()=>t(()=>import("./api-clients.html.d06fd0bd.js"),[]).then(({data:e})=>e),"v-3b29db1e":()=>t(()=>import("./metadata.html.23f4eb70.js"),[]).then(({data:e})=>e),"v-0cd91dd6":()=>t(()=>import("./models.html.b9811d9a.js"),[]).then(({data:e})=>e),"v-25ac4e91":()=>t(()=>import("./viewmodels.html.c2654093.js"),[]).then(({data:e})=>e),"v-131ad0c4":()=>t(()=>import("./c-admin-display.html.30c2d887.js"),[]).then(({data:e})=>e),"v-37e06bde":()=>t(()=>import("./c-admin-editor-page.html.179cc0d1.js"),[]).then(({data:e})=>e),"v-2a215065":()=>t(()=>import("./c-admin-editor.html.3d2a0299.js"),[]).then(({data:e})=>e),"v-2e0f2971":()=>t(()=>import("./c-admin-method.html.add7fa03.js"),[]).then(({data:e})=>e),"v-065dbaae":()=>t(()=>import("./c-admin-methods.html.4198ca7c.js"),[]).then(({data:e})=>e),"v-72d994c4":()=>t(()=>import("./c-admin-table-page.html.3938dbb7.js"),[]).then(({data:e})=>e),"v-6bac3238":()=>t(()=>import("./c-admin-table-toolbar.html.c459a0a0.js"),[]).then(({data:e})=>e),"v-335a61d2":()=>t(()=>import("./c-admin-table.html.698a17a1.js"),[]).then(({data:e})=>e),"v-0c72dc02":()=>t(()=>import("./c-datetime-picker.html.34960e92.js"),[]).then(({data:e})=>e),"v-76b68ec0":()=>t(()=>import("./c-display.html.61470104.js"),[]).then(({data:e})=>e),"v-82d04650":()=>t(()=>import("./c-input.html.d42429ba.js"),[]).then(({data:e})=>e),"v-417cf638":()=>t(()=>import("./c-list-filters.html.67503626.js"),[]).then(({data:e})=>e),"v-546d4960":()=>t(()=>import("./c-list-page-size.html.24864951.js"),[]).then(({data:e})=>e),"v-23f5be44":()=>t(()=>import("./c-list-page.html.ea7168b8.js"),[]).then(({data:e})=>e),"v-263706ce":()=>t(()=>import("./c-list-pagination.html.d2215a15.js"),[]).then(({data:e})=>e),"v-4822168d":()=>t(()=>import("./c-list-range-display.html.8f5b0583.js"),[]).then(({data:e})=>e),"v-ad853c14":()=>t(()=>import("./c-loader-status.html.4560dc1b.js"),[]).then(({data:e})=>e),"v-5c295e24":()=>t(()=>import("./c-select-many-to-many.html.3b1f5127.js"),[]).then(({data:e})=>e),"v-0227e47c":()=>t(()=>import("./c-select-string-value.html.92fa2e4d.js"),[]).then(({data:e})=>e),"v-1cab422f":()=>t(()=>import("./c-select-values.html.e35cce64.js"),[]).then(({data:e})=>e),"v-708bd4f4":()=>t(()=>import("./c-select.html.c5764dd7.js"),[]).then(({data:e})=>e),"v-1244db54":()=>t(()=>import("./c-table.html.6f5fc144.js"),[]).then(({data:e})=>e),"v-3706649a":()=>t(()=>import("./404.html.98e87f94.js"),[]).then(({data:e})=>e)},X=JSON.parse('{"base":"/Coalesce/","lang":"en-US","title":"Coalesce","description":"Documentation for IntelliTect.Coalesce","head":[],"locales":{}}'),P={"v-a7d34384":o(()=>t(()=>import("./history.html.1b31599a.js"),["assets/history.html.1b31599a.js","assets/framework.fe9a73df.js"])),"v-8daa1a0e":o(()=>t(()=>import("./index.html.5d377a5b.js"),["assets/index.html.5d377a5b.js","assets/framework.fe9a73df.js"])),"v-5ee46ccb":o(()=>t(()=>import("./include-tree.html.5aed11ea.js"),["assets/include-tree.html.5aed11ea.js","assets/framework.fe9a73df.js"])),"v-5d077123":o(()=>t(()=>import("./includes.html.08fa3b0f.js"),["assets/includes.html.08fa3b0f.js","assets/framework.fe9a73df.js"])),"v-6db94f86":o(()=>t(()=>import("./coalesce-json.html.5f8b8190.js"),["assets/coalesce-json.html.5f8b8190.js","assets/framework.fe9a73df.js"])),"v-74ba1c67":o(()=>t(()=>import("./security.html.df29655c.js"),["assets/security.html.df29655c.js","assets/framework.fe9a73df.js"])),"v-48c2b024":o(()=>t(()=>import("./startup.html.87b1cf60.js"),["assets/startup.html.87b1cf60.js","assets/framework.fe9a73df.js"])),"v-ad0e9b2c":o(()=>t(()=>import("./attributes.html.086d028f.js"),["assets/attributes.html.086d028f.js","assets/framework.fe9a73df.js"])),"v-ada1a0e0":o(()=>t(()=>import("./behaviors.html.9f165464.js"),["assets/behaviors.html.9f165464.js","assets/framework.fe9a73df.js"])),"v-2960e828":o(()=>t(()=>import("./data-sources.html.c1d3cc86.js"),["assets/data-sources.html.c1d3cc86.js","assets/framework.fe9a73df.js"])),"v-0f1d24df":o(()=>t(()=>import("./methods.html.c31b3de0.js"),["assets/methods.html.c31b3de0.js","assets/framework.fe9a73df.js"])),"v-57b8d80e":o(()=>t(()=>import("./properties.html.e0250356.js"),["assets/properties.html.e0250356.js","assets/framework.fe9a73df.js"])),"v-d0002c24":o(()=>t(()=>import("./dtos.html.897f99f4.js"),["assets/dtos.html.897f99f4.js","assets/framework.fe9a73df.js"])),"v-5f0df441":o(()=>t(()=>import("./entities.html.925a4840.js"),["assets/entities.html.925a4840.js","assets/framework.fe9a73df.js"])),"v-3d8f170b":o(()=>t(()=>import("./external-types.html.b5ed5417.js"),["assets/external-types.html.b5ed5417.js","assets/framework.fe9a73df.js"])),"v-d699f478":o(()=>t(()=>import("./services.html.1143f6c9.js"),["assets/services.html.1143f6c9.js","assets/framework.fe9a73df.js"])),"v-94b3a3fa":o(()=>t(()=>import("./dtos.html.7540bee6.js"),["assets/dtos.html.7540bee6.js","assets/framework.fe9a73df.js"])),"v-7231735f":o(()=>t(()=>import("./generation.html.d81ac009.js"),["assets/generation.html.d81ac009.js","assets/framework.fe9a73df.js"])),"v-ba2d632e":o(()=>t(()=>import("./getting-started-modeling.html.bcc466db.js"),["assets/getting-started-modeling.html.bcc466db.js","assets/framework.fe9a73df.js"])),"v-76e9aad4":o(()=>t(()=>import("./external-view-model.html.1df8fd42.js"),["assets/external-view-model.html.1df8fd42.js","assets/framework.fe9a73df.js"])),"v-32435eb2":o(()=>t(()=>import("./list-view-model.html.29d6ec3a.js"),["assets/list-view-model.html.29d6ec3a.js","assets/framework.fe9a73df.js"])),"v-0c9f605a":o(()=>t(()=>import("./view-model.html.51107651.js"),["assets/view-model.html.51107651.js","assets/framework.fe9a73df.js"])),"v-83dc9a7e":o(()=>t(()=>import("./getting-started.html.2295c26a.js"),["assets/getting-started.html.2295c26a.js","assets/framework.fe9a73df.js"])),"v-930615c0":o(()=>t(()=>import("./overview.html.f822ebee.js"),["assets/overview.html.f822ebee.js","assets/framework.fe9a73df.js"])),"v-91a9c356":o(()=>t(()=>import("./getting-started.html.5bf7a7d7.js"),["assets/getting-started.html.5bf7a7d7.js","assets/framework.fe9a73df.js"])),"v-4966afe8":o(()=>t(()=>import("./overview.html.f23b8be3.js"),["assets/overview.html.f23b8be3.js","assets/framework.fe9a73df.js"])),"v-6ffb6087":o(()=>t(()=>import("./vue2-to-vue3.html.afa6f987.js"),["assets/vue2-to-vue3.html.afa6f987.js","assets/framework.fe9a73df.js"])),"v-f401e784":o(()=>t(()=>import("./client-validation.html.3e200ba2.js"),["assets/client-validation.html.3e200ba2.js","assets/framework.fe9a73df.js"])),"v-19f90752":o(()=>t(()=>import("./coalesce.html.0b9c0e83.js"),["assets/coalesce.html.0b9c0e83.js","assets/framework.fe9a73df.js"])),"v-3903d1f2":o(()=>t(()=>import("./controller-action.html.c00be531.js"),["assets/controller-action.html.c00be531.js","assets/framework.fe9a73df.js"])),"v-10bb147d":o(()=>t(()=>import("./controller.html.5a450f80.js"),["assets/controller.html.5a450f80.js","assets/framework.fe9a73df.js"])),"v-6b92cba8":o(()=>t(()=>import("./create-controller.html.de1c3529.js"),["assets/create-controller.html.de1c3529.js","assets/framework.fe9a73df.js"])),"v-2ddfa8c0":o(()=>t(()=>import("./date-type.html.ae667ab6.js"),["assets/date-type.html.ae667ab6.js","assets/framework.fe9a73df.js"])),"v-28165697":o(()=>t(()=>import("./default-order-by.html.156c96e7.js"),["assets/default-order-by.html.156c96e7.js","assets/framework.fe9a73df.js"])),"v-ee3c31e8":o(()=>t(()=>import("./dto-includes-excludes.html.221ebe75.js"),["assets/dto-includes-excludes.html.221ebe75.js","assets/framework.fe9a73df.js"])),"v-1e992cc4":o(()=>t(()=>import("./execute.html.3ada74ae.js"),["assets/execute.html.3ada74ae.js","assets/framework.fe9a73df.js"])),"v-f75859a2":o(()=>t(()=>import("./hidden.html.bc0163d7.js"),["assets/hidden.html.bc0163d7.js","assets/framework.fe9a73df.js"])),"v-60941fa8":o(()=>t(()=>import("./inject.html.6b988db4.js"),["assets/inject.html.6b988db4.js","assets/framework.fe9a73df.js"])),"v-440fbe82":o(()=>t(()=>import("./internal-use.html.d0d63e3d.js"),["assets/internal-use.html.d0d63e3d.js","assets/framework.fe9a73df.js"])),"v-05ce079d":o(()=>t(()=>import("./list-text.html.29c3267b.js"),["assets/list-text.html.29c3267b.js","assets/framework.fe9a73df.js"])),"v-e0635f52":o(()=>t(()=>import("./load-from-data-source.html.37349d94.js"),["assets/load-from-data-source.html.37349d94.js","assets/framework.fe9a73df.js"])),"v-d6277114":o(()=>t(()=>import("./many-to-many.html.806b3ca1.js"),["assets/many-to-many.html.806b3ca1.js","assets/framework.fe9a73df.js"])),"v-d3094c1e":o(()=>t(()=>import("./search.html.9174939a.js"),["assets/search.html.9174939a.js","assets/framework.fe9a73df.js"])),"v-492e3baa":o(()=>t(()=>import("./security-attribute.html.aaba8860.js"),["assets/security-attribute.html.aaba8860.js","assets/framework.fe9a73df.js"])),"v-3a375f50":o(()=>t(()=>import("./select-filter.html.0de35596.js"),["assets/select-filter.html.0de35596.js","assets/framework.fe9a73df.js"])),"v-1a8eae00":o(()=>t(()=>import("./typescript-partial.html.544452e0.js"),["assets/typescript-partial.html.544452e0.js","assets/framework.fe9a73df.js"])),"v-024b84b2":o(()=>t(()=>import("./bindings.html.3ecb74ed.js"),["assets/bindings.html.3ecb74ed.js","assets/framework.fe9a73df.js"])),"v-1edd1d5a":o(()=>t(()=>import("./external-view-model.html.c346f050.js"),["assets/external-view-model.html.c346f050.js","assets/framework.fe9a73df.js"])),"v-30cbf62d":o(()=>t(()=>import("./list-view-model.html.c0ac40ce.js"),["assets/list-view-model.html.c0ac40ce.js","assets/framework.fe9a73df.js"])),"v-0f867a4b":o(()=>t(()=>import("./methods.html.2354376c.js"),["assets/methods.html.2354376c.js","assets/framework.fe9a73df.js"])),"v-275d16cf":o(()=>t(()=>import("./model-config.html.e7f5edd8.js"),["assets/model-config.html.e7f5edd8.js","assets/framework.fe9a73df.js"])),"v-0f1441d8":o(()=>t(()=>import("./view-model.html.f673f519.js"),["assets/view-model.html.f673f519.js","assets/framework.fe9a73df.js"])),"v-33aa6bf4":o(()=>t(()=>import("./overview.html.48ae847f.js"),["assets/overview.html.48ae847f.js","assets/framework.fe9a73df.js"])),"v-4a7e6fdd":o(()=>t(()=>import("./api-clients.html.3a38f66c.js"),["assets/api-clients.html.3a38f66c.js","assets/framework.fe9a73df.js"])),"v-3b29db1e":o(()=>t(()=>import("./metadata.html.cbe35d07.js"),["assets/metadata.html.cbe35d07.js","assets/framework.fe9a73df.js"])),"v-0cd91dd6":o(()=>t(()=>import("./models.html.598e23b6.js"),["assets/models.html.598e23b6.js","assets/framework.fe9a73df.js"])),"v-25ac4e91":o(()=>t(()=>import("./viewmodels.html.db1146d4.js"),["assets/viewmodels.html.db1146d4.js","assets/framework.fe9a73df.js"])),"v-131ad0c4":o(()=>t(()=>import("./c-admin-display.html.a866e748.js"),["assets/c-admin-display.html.a866e748.js","assets/framework.fe9a73df.js"])),"v-37e06bde":o(()=>t(()=>import("./c-admin-editor-page.html.bf671050.js"),["assets/c-admin-editor-page.html.bf671050.js","assets/framework.fe9a73df.js"])),"v-2a215065":o(()=>t(()=>import("./c-admin-editor.html.34b7a2e3.js"),["assets/c-admin-editor.html.34b7a2e3.js","assets/framework.fe9a73df.js"])),"v-2e0f2971":o(()=>t(()=>import("./c-admin-method.html.7f900f5d.js"),["assets/c-admin-method.html.7f900f5d.js","assets/framework.fe9a73df.js"])),"v-065dbaae":o(()=>t(()=>import("./c-admin-methods.html.98c2a74b.js"),["assets/c-admin-methods.html.98c2a74b.js","assets/framework.fe9a73df.js"])),"v-72d994c4":o(()=>t(()=>import("./c-admin-table-page.html.9501ce38.js"),["assets/c-admin-table-page.html.9501ce38.js","assets/framework.fe9a73df.js"])),"v-6bac3238":o(()=>t(()=>import("./c-admin-table-toolbar.html.20a09678.js"),["assets/c-admin-table-toolbar.html.20a09678.js","assets/framework.fe9a73df.js"])),"v-335a61d2":o(()=>t(()=>import("./c-admin-table.html.b1ffc448.js"),["assets/c-admin-table.html.b1ffc448.js","assets/framework.fe9a73df.js"])),"v-0c72dc02":o(()=>t(()=>import("./c-datetime-picker.html.061c28e5.js"),["assets/c-datetime-picker.html.061c28e5.js","assets/framework.fe9a73df.js"])),"v-76b68ec0":o(()=>t(()=>import("./c-display.html.dabbb75d.js"),["assets/c-display.html.dabbb75d.js","assets/framework.fe9a73df.js"])),"v-82d04650":o(()=>t(()=>import("./c-input.html.e13a8a1c.js"),["assets/c-input.html.e13a8a1c.js","assets/framework.fe9a73df.js"])),"v-417cf638":o(()=>t(()=>import("./c-list-filters.html.91c1f54c.js"),["assets/c-list-filters.html.91c1f54c.js","assets/framework.fe9a73df.js"])),"v-546d4960":o(()=>t(()=>import("./c-list-page-size.html.59698aa9.js"),["assets/c-list-page-size.html.59698aa9.js","assets/framework.fe9a73df.js"])),"v-23f5be44":o(()=>t(()=>import("./c-list-page.html.4b1df619.js"),["assets/c-list-page.html.4b1df619.js","assets/framework.fe9a73df.js"])),"v-263706ce":o(()=>t(()=>import("./c-list-pagination.html.8d277ca9.js"),["assets/c-list-pagination.html.8d277ca9.js","assets/framework.fe9a73df.js"])),"v-4822168d":o(()=>t(()=>import("./c-list-range-display.html.78677805.js"),["assets/c-list-range-display.html.78677805.js","assets/framework.fe9a73df.js"])),"v-ad853c14":o(()=>t(()=>import("./c-loader-status.html.00e079dd.js"),["assets/c-loader-status.html.00e079dd.js","assets/framework.fe9a73df.js"])),"v-5c295e24":o(()=>t(()=>import("./c-select-many-to-many.html.4b1a2c18.js"),["assets/c-select-many-to-many.html.4b1a2c18.js","assets/framework.fe9a73df.js"])),"v-0227e47c":o(()=>t(()=>import("./c-select-string-value.html.1351504b.js"),["assets/c-select-string-value.html.1351504b.js","assets/framework.fe9a73df.js"])),"v-1cab422f":o(()=>t(()=>import("./c-select-values.html.962e342c.js"),["assets/c-select-values.html.962e342c.js","assets/framework.fe9a73df.js"])),"v-708bd4f4":o(()=>t(()=>import("./c-select.html.44d765fd.js"),["assets/c-select.html.44d765fd.js","assets/framework.fe9a73df.js"])),"v-1244db54":o(()=>t(()=>import("./c-table.html.fc5ab5de.js"),["assets/c-table.html.fc5ab5de.js","assets/framework.fe9a73df.js"])),"v-3706649a":o(()=>t(()=>import("./404.html.4f7b27d6.js"),["assets/404.html.4f7b27d6.js","assets/framework.fe9a73df.js"]))};var ee=f(Q),V=S({key:"",path:"",title:"",lang:"",frontmatter:{},excerpt:"",headers:[]}),v=f(V),O=()=>v,A=Symbol(""),De=()=>{const e=g(A);if(!e)throw new Error("usePageFrontmatter() is called without provider.");return e},R=Symbol(""),te=()=>{const e=g(R);if(!e)throw new Error("usePageHead() is called without provider.");return e},oe=Symbol(""),k=Symbol(""),ie=()=>{const e=g(k);if(!e)throw new Error("usePageLang() is called without provider.");return e},I=Symbol(""),Pe=()=>{const e=g(I);if(!e)throw new Error("useRouteLocale() is called without provider.");return e},u=f(X),ae=()=>u,w=Symbol(""),Ve=()=>{const e=g(w);if(!e)throw new Error("useSiteLocaleData() is called without provider.");return e},se=Symbol(""),_=C({resolvePageData:async e=>{const i=ee.value[e],s=await(i==null?void 0:i());return s!=null?s:V},resolvePageFrontmatter:e=>e.frontmatter,resolvePageHead:(e,i,s)=>{const a=p(i.description)?i.description:s.description,c=[...x(i.head)?i.head:[],...s.head,["title",{},e],["meta",{name:"description",content:a}]];return $(c)},resolvePageHeadTitle:(e,i)=>`${e.title?`${e.title}`:""}${i.title?` | ${i.title}`:""}`,resolvePageLang:e=>e.lang||"en",resolveRouteLocale:(e,i)=>H(e,i),resolveSiteLocaleData:(e,i)=>({...e,...e.locales[i]})}),ne=L({name:"ClientOnly",setup(e,i){const s=f(!1);return D(()=>{s.value=!0}),()=>{var a,c;return s.value?(c=(a=i.slots).default)==null?void 0:c.call(a):null}}}),ce=L({name:"Content",props:{pageKey:{type:String,required:!1,default:""}},setup(e){const i=O(),s=d(()=>P[e.pageKey||i.value.key]);return()=>s.value?h(s.value):h("div","404 Not Found")}}),re=e=>M(e)?e:`${ae().value.base}${j(e)}`;const le=[["v-a7d34384","/history.html",{title:"Welcome to Coalesce's documentation!"},["/history","/history.md"]],["v-8daa1a0e","/",{title:"Coalesce Documentation"},["/index.html","/index.md"]],["v-5ee46ccb","/concepts/include-tree.html",{title:"Include Tree"},["/concepts/include-tree","/concepts/include-tree.md"]],["v-5d077123","/concepts/includes.html",{title:"Includes String"},["/concepts/includes","/concepts/includes.md"]],["v-6db94f86","/topics/coalesce-json.html",{title:"Code Generation Configuration"},["/topics/coalesce-json","/topics/coalesce-json.md"]],["v-74ba1c67","/topics/security.html",{title:"Security"},["/topics/security","/topics/security.md"]],["v-48c2b024","/topics/startup.html",{title:"Application Configuration"},["/topics/startup","/topics/startup.md"]],["v-ad0e9b2c","/modeling/model-components/attributes.html",{title:"Attributes"},["/modeling/model-components/attributes","/modeling/model-components/attributes.md"]],["v-ada1a0e0","/modeling/model-components/behaviors.html",{title:"Behaviors"},["/modeling/model-components/behaviors","/modeling/model-components/behaviors.md"]],["v-2960e828","/modeling/model-components/data-sources.html",{title:"Data Sources"},["/modeling/model-components/data-sources","/modeling/model-components/data-sources.md"]],["v-0f1d24df","/modeling/model-components/methods.html",{title:"Methods"},["/modeling/model-components/methods","/modeling/model-components/methods.md"]],["v-57b8d80e","/modeling/model-components/properties.html",{title:"Properties"},["/modeling/model-components/properties","/modeling/model-components/properties.md"]],["v-d0002c24","/modeling/model-types/dtos.html",{title:"Custom DTOs"},["/modeling/model-types/dtos","/modeling/model-types/dtos.md"]],["v-5f0df441","/modeling/model-types/entities.html",{title:"Entity Models"},["/modeling/model-types/entities","/modeling/model-types/entities.md"]],["v-3d8f170b","/modeling/model-types/external-types.html",{title:"External Types"},["/modeling/model-types/external-types","/modeling/model-types/external-types.md"]],["v-d699f478","/modeling/model-types/services.html",{title:"Services"},["/modeling/model-types/services","/modeling/model-types/services.md"]],["v-94b3a3fa","/stacks/agnostic/dtos.html",{title:"Generated C# DTOs"},["/stacks/agnostic/dtos","/stacks/agnostic/dtos.md"]],["v-7231735f","/stacks/agnostic/generation.html",{title:"Code Generation Overview"},["/stacks/agnostic/generation","/stacks/agnostic/generation.md"]],["v-ba2d632e","/stacks/agnostic/getting-started-modeling.html",{title:"Data Modeling"},["/stacks/agnostic/getting-started-modeling","/stacks/agnostic/getting-started-modeling.md"]],["v-76e9aad4","/stacks/disambiguation/external-view-model.html",{title:"TypeScript External ViewModels"},["/stacks/disambiguation/external-view-model","/stacks/disambiguation/external-view-model.md"]],["v-32435eb2","/stacks/disambiguation/list-view-model.html",{title:"TypeScript List ViewModels"},["/stacks/disambiguation/list-view-model","/stacks/disambiguation/list-view-model.md"]],["v-0c9f605a","/stacks/disambiguation/view-model.html",{title:"TypeScript ViewModels"},["/stacks/disambiguation/view-model","/stacks/disambiguation/view-model.md"]],["v-83dc9a7e","/stacks/ko/getting-started.html",{title:"Getting Started with Knockout"},["/stacks/ko/getting-started","/stacks/ko/getting-started.md"]],["v-930615c0","/stacks/ko/overview.html",{title:"Knockout Overview"},["/stacks/ko/overview","/stacks/ko/overview.md"]],["v-91a9c356","/stacks/vue/getting-started.html",{title:"Getting Started with Vue"},["/stacks/vue/getting-started","/stacks/vue/getting-started.md"]],["v-4966afe8","/stacks/vue/overview.html",{title:"Vue Overview"},["/stacks/vue/overview","/stacks/vue/overview.md"]],["v-6ffb6087","/stacks/vue/vue2-to-vue3.html",{title:"Vue 2 to Vue 3"},["/stacks/vue/vue2-to-vue3","/stacks/vue/vue2-to-vue3.md"]],["v-f401e784","/modeling/model-components/attributes/client-validation.html",{title:"[ClientValidation]"},["/modeling/model-components/attributes/client-validation","/modeling/model-components/attributes/client-validation.md"]],["v-19f90752","/modeling/model-components/attributes/coalesce.html",{title:"[Coalesce]"},["/modeling/model-components/attributes/coalesce","/modeling/model-components/attributes/coalesce.md"]],["v-3903d1f2","/modeling/model-components/attributes/controller-action.html",{title:"[ControllerAction]"},["/modeling/model-components/attributes/controller-action","/modeling/model-components/attributes/controller-action.md"]],["v-10bb147d","/modeling/model-components/attributes/controller.html",{title:"[Controller]"},["/modeling/model-components/attributes/controller","/modeling/model-components/attributes/controller.md"]],["v-6b92cba8","/modeling/model-components/attributes/create-controller.html",{title:"[CreateController]"},["/modeling/model-components/attributes/create-controller","/modeling/model-components/attributes/create-controller.md"]],["v-2ddfa8c0","/modeling/model-components/attributes/date-type.html",{title:"[DateType]"},["/modeling/model-components/attributes/date-type","/modeling/model-components/attributes/date-type.md"]],["v-28165697","/modeling/model-components/attributes/default-order-by.html",{title:"[DefaultOrderBy]"},["/modeling/model-components/attributes/default-order-by","/modeling/model-components/attributes/default-order-by.md"]],["v-ee3c31e8","/modeling/model-components/attributes/dto-includes-excludes.html",{title:"[DtoIncludes] & [DtoExcludes]"},["/modeling/model-components/attributes/dto-includes-excludes","/modeling/model-components/attributes/dto-includes-excludes.md"]],["v-1e992cc4","/modeling/model-components/attributes/execute.html",{title:"[Execute]"},["/modeling/model-components/attributes/execute","/modeling/model-components/attributes/execute.md"]],["v-f75859a2","/modeling/model-components/attributes/hidden.html",{title:"[Hidden]"},["/modeling/model-components/attributes/hidden","/modeling/model-components/attributes/hidden.md"]],["v-60941fa8","/modeling/model-components/attributes/inject.html",{title:"[Inject]"},["/modeling/model-components/attributes/inject","/modeling/model-components/attributes/inject.md"]],["v-440fbe82","/modeling/model-components/attributes/internal-use.html",{title:"[InternalUse]"},["/modeling/model-components/attributes/internal-use","/modeling/model-components/attributes/internal-use.md"]],["v-05ce079d","/modeling/model-components/attributes/list-text.html",{title:"[ListText]"},["/modeling/model-components/attributes/list-text","/modeling/model-components/attributes/list-text.md"]],["v-e0635f52","/modeling/model-components/attributes/load-from-data-source.html",{title:"[LoadFromDataSource]"},["/modeling/model-components/attributes/load-from-data-source","/modeling/model-components/attributes/load-from-data-source.md"]],["v-d6277114","/modeling/model-components/attributes/many-to-many.html",{title:"[ManyToMany]"},["/modeling/model-components/attributes/many-to-many","/modeling/model-components/attributes/many-to-many.md"]],["v-d3094c1e","/modeling/model-components/attributes/search.html",{title:"[Search]"},["/modeling/model-components/attributes/search","/modeling/model-components/attributes/search.md"]],["v-492e3baa","/modeling/model-components/attributes/security-attribute.html",{title:"Security Attributes"},["/modeling/model-components/attributes/security-attribute","/modeling/model-components/attributes/security-attribute.md"]],["v-3a375f50","/modeling/model-components/attributes/select-filter.html",{title:"[SelectFilter]"},["/modeling/model-components/attributes/select-filter","/modeling/model-components/attributes/select-filter.md"]],["v-1a8eae00","/modeling/model-components/attributes/typescript-partial.html",{title:"[TypeScriptPartial]"},["/modeling/model-components/attributes/typescript-partial","/modeling/model-components/attributes/typescript-partial.md"]],["v-024b84b2","/stacks/ko/client/bindings.html",{title:"Knockout Bindings"},["/stacks/ko/client/bindings","/stacks/ko/client/bindings.md"]],["v-1edd1d5a","/stacks/ko/client/external-view-model.html",{title:"TypeScript External ViewModels"},["/stacks/ko/client/external-view-model","/stacks/ko/client/external-view-model.md"]],["v-30cbf62d","/stacks/ko/client/list-view-model.html",{title:"TypeScript ListViewModels"},["/stacks/ko/client/list-view-model","/stacks/ko/client/list-view-model.md"]],["v-0f867a4b","/stacks/ko/client/methods.html",{title:"TypeScript Method Objects"},["/stacks/ko/client/methods","/stacks/ko/client/methods.md"]],["v-275d16cf","/stacks/ko/client/model-config.html",{title:"ViewModel Configuration"},["/stacks/ko/client/model-config","/stacks/ko/client/model-config.md"]],["v-0f1441d8","/stacks/ko/client/view-model.html",{title:"TypeScript ViewModels"},["/stacks/ko/client/view-model","/stacks/ko/client/view-model.md"]],["v-33aa6bf4","/stacks/vue/coalesce-vue-vuetify/overview.html",{title:"Vuetify Components"},["/stacks/vue/coalesce-vue-vuetify/overview","/stacks/vue/coalesce-vue-vuetify/overview.md"]],["v-4a7e6fdd","/stacks/vue/layers/api-clients.html",{title:"Vue API Client Layer"},["/stacks/vue/layers/api-clients","/stacks/vue/layers/api-clients.md"]],["v-3b29db1e","/stacks/vue/layers/metadata.html",{title:"Vue Metadata Layer"},["/stacks/vue/layers/metadata","/stacks/vue/layers/metadata.md"]],["v-0cd91dd6","/stacks/vue/layers/models.html",{title:"Vue Model Layer"},["/stacks/vue/layers/models","/stacks/vue/layers/models.md"]],["v-25ac4e91","/stacks/vue/layers/viewmodels.html",{title:"Vue ViewModel Layer"},["/stacks/vue/layers/viewmodels","/stacks/vue/layers/viewmodels.md"]],["v-131ad0c4","/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html",{title:"c-admin-display"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-display","/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.md"]],["v-37e06bde","/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html",{title:"c-admin-editor-page"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page","/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.md"]],["v-2a215065","/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html",{title:"c-admin-editor"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor","/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.md"]],["v-2e0f2971","/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html",{title:"c-admin-method"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-method","/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.md"]],["v-065dbaae","/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html",{title:"c-admin-methods"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods","/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.md"]],["v-72d994c4","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html",{title:"c-admin-table-page"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.md"]],["v-6bac3238","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html",{title:"c-admin-table-toolbar"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.md"]],["v-335a61d2","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html",{title:"c-admin-table"},["/stacks/vue/coalesce-vue-vuetify/components/c-admin-table","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.md"]],["v-0c72dc02","/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html",{title:"c-datetime-picker"},["/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker","/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.md"]],["v-76b68ec0","/stacks/vue/coalesce-vue-vuetify/components/c-display.html",{title:"c-display"},["/stacks/vue/coalesce-vue-vuetify/components/c-display","/stacks/vue/coalesce-vue-vuetify/components/c-display.md"]],["v-82d04650","/stacks/vue/coalesce-vue-vuetify/components/c-input.html",{title:"c-input"},["/stacks/vue/coalesce-vue-vuetify/components/c-input","/stacks/vue/coalesce-vue-vuetify/components/c-input.md"]],["v-417cf638","/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html",{title:"c-list-filters"},["/stacks/vue/coalesce-vue-vuetify/components/c-list-filters","/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.md"]],["v-546d4960","/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html",{title:"c-list-page-size"},["/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size","/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.md"]],["v-23f5be44","/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html",{title:"c-list-page"},["/stacks/vue/coalesce-vue-vuetify/components/c-list-page","/stacks/vue/coalesce-vue-vuetify/components/c-list-page.md"]],["v-263706ce","/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html",{title:"c-list-pagination"},["/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination","/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.md"]],["v-4822168d","/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html",{title:"c-list-range-display"},["/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display","/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.md"]],["v-ad853c14","/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html",{title:"c-loader-status"},["/stacks/vue/coalesce-vue-vuetify/components/c-loader-status","/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.md"]],["v-5c295e24","/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html",{title:"c-select-many-to-many"},["/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many","/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md"]],["v-0227e47c","/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.html",{title:"c-select-string-value"},["/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value","/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.md"]],["v-1cab422f","/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html",{title:"c-select-values"},["/stacks/vue/coalesce-vue-vuetify/components/c-select-values","/stacks/vue/coalesce-vue-vuetify/components/c-select-values.md"]],["v-708bd4f4","/stacks/vue/coalesce-vue-vuetify/components/c-select.html",{title:"c-select"},["/stacks/vue/coalesce-vue-vuetify/components/c-select","/stacks/vue/coalesce-vue-vuetify/components/c-select.md"]],["v-1244db54","/stacks/vue/coalesce-vue-vuetify/components/c-table.html",{title:"c-table"},["/stacks/vue/coalesce-vue-vuetify/components/c-table","/stacks/vue/coalesce-vue-vuetify/components/c-table.md"]],["v-3706649a","/404.html",{title:""},["/404"]]];var de="Layout",me="NotFound",_e=async()=>{const{clientConfigs:e}=await t(()=>import("./clientConfigs.922cb10d.js"),["assets/clientConfigs.922cb10d.js","assets/framework.fe9a73df.js"]),i=e.reduce((a,c)=>({...a,...c.layouts}),{});return L({name:"Vuepress",setup(){const a=O(),c=d(()=>{let n;if(a.value.path){const r=a.value.frontmatter.layout;p(r)?n=r:n=de}else n=me;return i[n]});return()=>h(c.value)}})},ve=async()=>{const e=await _e();return le.reduce((i,[s,a,c,n])=>(i.push({name:s,path:a,component:e,meta:c},...n.map(r=>({path:r,redirect:a}))),i),[{name:"404",path:"/:catchAll(.*)",component:e}])},ue=z,pe=async()=>{const e=B({history:ue(U(u.value.base)),routes:await ve(),scrollBehavior:(i,s,a)=>a||(i.hash?{el:i.hash}:{top:0})});return e.beforeResolve(async(i,s)=>{var a;(i.path!==s.path||s===G)&&([v.value]=await Promise.all([_.resolvePageData(i.name),(a=P[i.name])==null?void 0:a.__asyncLoader()]))}),e},Ee=e=>{e.component("ClientOnly",ne),e.component("Content",ce)},he=(e,i)=>{const s=d(()=>_.resolveRouteLocale(u.value.locales,i.currentRoute.value.path)),a=d(()=>_.resolveSiteLocaleData(u.value,s.value)),c=d(()=>_.resolvePageFrontmatter(v.value)),n=d(()=>_.resolvePageHeadTitle(v.value,a.value)),r=d(()=>_.resolvePageHead(n.value,c.value,a.value)),l=d(()=>_.resolvePageLang(v.value));return e.provide(I,s),e.provide(w,a),e.provide(A,c),e.provide(oe,n),e.provide(R,r),e.provide(k,l),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>c.value},$head:{get:()=>r.value},$headTitle:{get:()=>n.value},$lang:{get:()=>l.value},$page:{get:()=>v.value},$routeLocale:{get:()=>s.value},$site:{get:()=>u.value},$siteLocale:{get:()=>a.value},$withBase:{get:()=>re}}),{pageData:v,pageFrontmatter:c,pageHead:r,pageHeadTitle:n,pageLang:l,routeLocale:s,siteData:u,siteLocaleData:a}},fe=()=>{const e=q(),i=te(),s=ie(),a=f([]),c=()=>{i.value.forEach(r=>{const l=ge(r);l&&a.value.push(l)})},n=()=>{document.documentElement.lang=s.value,a.value.forEach(r=>{r.parentNode===document.head&&document.head.removeChild(r)}),a.value.splice(0,a.value.length),i.value.forEach(r=>{const l=be(r);l!==null&&(document.head.appendChild(l),a.value.push(l))})};J(se,n),D(()=>{c(),n(),K(()=>e.path,()=>n())})},ge=([e,i,s=""])=>{const a=Object.entries(i).map(([l,b])=>p(b)?`[${l}=${JSON.stringify(b)}]`:b===!0?`[${l}]`:"").join(""),c=`head > ${e}${a}`;return Array.from(document.querySelectorAll(c)).find(l=>l.innerText===s)||null},be=([e,i,s])=>{if(!p(e))return null;const a=document.createElement(e);return W(i)&&Object.entries(i).forEach(([c,n])=>{p(n)?a.setAttribute(c,n):n===!0&&a.setAttribute(c,"")}),p(s)&&a.appendChild(document.createTextNode(s)),a},ye=F,Le=async()=>{var a;const{clientConfigs:e}=await t(()=>import("./clientConfigs.922cb10d.js"),["assets/clientConfigs.922cb10d.js","assets/framework.fe9a73df.js"]),i=ye({name:"VuepressApp",setup(){var c;fe();for(const n of e)(c=n.setup)==null||c.call(n);return()=>[h(N),...e.flatMap(({rootComponents:n=[]})=>n.map(r=>h(r)))]}}),s=await pe();Ee(i),he(i,s);for(const c of e)await((a=c.enhance)==null?void 0:a.call(c,{app:i,router:s,siteData:u}));return i.use(s),{app:i,router:s}};Le().then(({app:e,router:i})=>{i.isReady().then(()=>{e.mount("#app")})});export{ne as C,t as _,De as a,O as b,ae as c,Le as createVueApp,Ve as d,ie as e,I as r,Pe as u,re as w}; diff --git a/assets/attributes.html.086d028f.js b/assets/attributes.html.086d028f.js new file mode 100644 index 000000000..08f790978 --- /dev/null +++ b/assets/attributes.html.086d028f.js @@ -0,0 +1 @@ +import{_ as s,y as d,z as h,X as t,Q as a,$ as o,B as e,W as c,a5 as u,P as r}from"./framework.fe9a73df.js";const p={},m=t("h1",{id:"attributes",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#attributes","aria-hidden":"true"},"#"),e(" Attributes")],-1),_=t("p",null,[e("Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from "),t("code",null,"System.ComponentModel.DataAnnotations"),e(".")],-1),f={class:"table-of-contents"},b=t("h2",{id:"coalesce-attributes",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#coalesce-attributes","aria-hidden":"true"},"#"),e(" Coalesce Attributes")],-1),y=t("p",null,"Browse the list in the sidebar to learn about the attributes that Coalesce provides that can be used to decorate your models.",-1),v=u('

ComponentModel Attributes

Coalesce also supports a number of the built-in System.ComponentModel.DataAnnotations attributes and will use these to shape the generated code.

[Display]

The displayed name and description of a property, as well as the order in which it appears in generated views, can be set via the [Display] attribute. By default, properties will be displayed in the order in which they are defined in their class.

[DisplayName]

The displayed name of a property can also be set via the [DisplayName] attribute.

[Required]

',7),g=t("code",null,"[Required]",-1),w=t("h3",{id:"range",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#range","aria-hidden":"true"},"#"),e(" [Range]")],-1),x=t("code",null,"[Range]",-1),k=t("h3",{id:"minlength",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#minlength","aria-hidden":"true"},"#"),e(" [MinLength]")],-1),C=t("code",null,"[MinLength]",-1),D=t("h3",{id:"maxlength",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#maxlength","aria-hidden":"true"},"#"),e(" [MaxLength]")],-1),M=t("code",null,"[MaxLength]",-1),N=t("h3",{id:"datatype",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#datatype","aria-hidden":"true"},"#"),e(" [DataType]")],-1),L=t("code",null,"DataType",-1),R=t("code",null,"DataTypeAttribute",-1),S=t("code",null,"string",-1),P=t("h3",{id:"foreignkey",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#foreignkey","aria-hidden":"true"},"#"),e(" [ForeignKey]")],-1),A=t("code",null,"[ForeignKey]",-1),B={href:"https://docs.microsoft.com/en-us/ef/core/modeling/relationships",target:"_blank",rel:"noopener noreferrer"},E=t("h3",{id:"inverseproperty",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#inverseproperty","aria-hidden":"true"},"#"),e(" [InverseProperty]")],-1),F={href:"https://docs.microsoft.com/en-us/ef/core/modeling/relationships",target:"_blank",rel:"noopener noreferrer"},T=t("code",null,"[InverseProperty]",-1),q=t("h3",{id:"databasegenerated",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#databasegenerated","aria-hidden":"true"},"#"),e(" [DatabaseGenerated]")],-1),I=t("code",null,"[DatabaseGenerated(DatabaseGeneratedOption.None)]",-1),V=t("h3",{id:"notmapped",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#notmapped","aria-hidden":"true"},"#"),e(" [NotMapped]")],-1),K=t("code",null,"[NotMapped]",-1);function G(O,z){const i=r("router-link"),n=r("RouterLink"),l=r("ExternalLinkIcon");return d(),h("div",null,[m,_,t("nav",f,[t("ul",null,[t("li",null,[a(i,{to:"#coalesce-attributes"},{default:o(()=>[e("Coalesce Attributes")]),_:1})]),t("li",null,[a(i,{to:"#componentmodel-attributes"},{default:o(()=>[e("ComponentModel Attributes")]),_:1}),t("ul",null,[t("li",null,[a(i,{to:"#display"},{default:o(()=>[e("[Display]")]),_:1})]),t("li",null,[a(i,{to:"#displayname"},{default:o(()=>[e("[DisplayName]")]),_:1})]),t("li",null,[a(i,{to:"#required"},{default:o(()=>[e("[Required]")]),_:1})]),t("li",null,[a(i,{to:"#range"},{default:o(()=>[e("[Range]")]),_:1})]),t("li",null,[a(i,{to:"#minlength"},{default:o(()=>[e("[MinLength]")]),_:1})]),t("li",null,[a(i,{to:"#maxlength"},{default:o(()=>[e("[MaxLength]")]),_:1})]),t("li",null,[a(i,{to:"#datatype"},{default:o(()=>[e("[DataType]")]),_:1})]),t("li",null,[a(i,{to:"#foreignkey"},{default:o(()=>[e("[ForeignKey]")]),_:1})]),t("li",null,[a(i,{to:"#inverseproperty"},{default:o(()=>[e("[InverseProperty]")]),_:1})]),t("li",null,[a(i,{to:"#databasegenerated"},{default:o(()=>[e("[DatabaseGenerated]")]),_:1})]),t("li",null,[a(i,{to:"#notmapped"},{default:o(()=>[e("[NotMapped]")]),_:1})])])])])]),b,y,c(" TODO: Is there some kind of metadata we can use to dynamically source the coalesce attribute page and list them here instead of directing the reader to the sidebar? "),v,t("p",null,[e("Properties with "),g,e(" will generate "),a(n,{to:"/modeling/model-components/attributes/client-validation.html"},{default:o(()=>[e("client validation")]),_:1}),e(" and "),a(n,{to:"/topics/security.html#server-side-data-validation"},{default:o(()=>[e("server validation")]),_:1}),e(" rules.")]),w,t("p",null,[e("Properties with "),x,e(" will generate "),a(n,{to:"/modeling/model-components/attributes/client-validation.html"},{default:o(()=>[e("client validation")]),_:1}),e(" and "),a(n,{to:"/topics/security.html#server-side-data-validation"},{default:o(()=>[e("server validation")]),_:1}),e(" rules.")]),k,t("p",null,[e("Properties with "),C,e(" will generate "),a(n,{to:"/modeling/model-components/attributes/client-validation.html"},{default:o(()=>[e("client validation")]),_:1}),e(" and "),a(n,{to:"/topics/security.html#server-side-data-validation"},{default:o(()=>[e("server validation")]),_:1}),e(" rules.")]),D,t("p",null,[e("Properties with "),M,e(" will generate "),a(n,{to:"/modeling/model-components/attributes/client-validation.html"},{default:o(()=>[e("client validation")]),_:1}),e(" and "),a(n,{to:"/topics/security.html#server-side-data-validation"},{default:o(()=>[e("server validation")]),_:1}),e(" rules.")]),N,t("p",null,[e("Some values of "),L,e(" when provided to "),R,e(" on a "),S,e(" property will alter the behavior of the "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:o(()=>[e("Vue Components")]),_:1}),e(". See "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:o(()=>[e("c-display")]),_:1}),e(" and See "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:o(()=>[e("c-display")]),_:1}),e(" for details.")]),P,t("p",null,[e("Normally, Coalesce figures out which properties are foreign keys, but if you don't use standard EF naming conventions then you'll need to annotate with "),A,e(" to help out both EF and Coalesce. See the "),t("a",B,[e("Entity Framework Relationships"),a(l)]),e(" documentation for more.")]),E,t("p",null,[e("Sometimes, Coalesce (and EF, too) can have trouble figuring out what the foreign key is supposed to be for a collection navigation property. See the "),t("a",F,[e("Entity Framework Relationships"),a(l)]),e(" documentation for details on how and why to use "),T,e(".")]),q,t("p",null,[e("Primary Keys with "),I,e(" will be settable on the client and will be appropriately handled by the "),a(n,{to:"/modeling/model-components/behaviors.html#standard-behaviors"},{default:o(()=>[e("Standard Behaviors")]),_:1}),e(" on the server. Unsupported on the "),a(n,{to:"/stacks/ko/overview.html"},{default:o(()=>[e("Knockout front-end stack")]),_:1}),e(".")]),V,t("p",null,[e("Model properties that aren't mapped to the database should be marked with "),K,e(" so that Coalesce doesn't try to load them from the database when "),a(n,{to:"/modeling/model-components/attributes/search.html"},{default:o(()=>[e("searching")]),_:1}),e(" or carrying out the "),a(n,{to:"/modeling/model-components/data-sources.html#default-loading-behavior"},{default:o(()=>[e("Default Loading Behavior")]),_:1}),e(".")])])}const U=s(p,[["render",G],["__file","attributes.html.vue"]]);export{U as default}; diff --git a/assets/attributes.html.0e4b72e3.js b/assets/attributes.html.0e4b72e3.js new file mode 100644 index 000000000..cbb1dd8c5 --- /dev/null +++ b/assets/attributes.html.0e4b72e3.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ad0e9b2c","path":"/modeling/model-components/attributes.html","title":"Attributes","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Coalesce Attributes","slug":"coalesce-attributes","link":"#coalesce-attributes","children":[]},{"level":2,"title":"ComponentModel Attributes","slug":"componentmodel-attributes","link":"#componentmodel-attributes","children":[{"level":3,"title":"[Display]","slug":"display","link":"#display","children":[]},{"level":3,"title":"[DisplayName]","slug":"displayname","link":"#displayname","children":[]},{"level":3,"title":"[Required]","slug":"required","link":"#required","children":[]},{"level":3,"title":"[Range]","slug":"range","link":"#range","children":[]},{"level":3,"title":"[MinLength]","slug":"minlength","link":"#minlength","children":[]},{"level":3,"title":"[MaxLength]","slug":"maxlength","link":"#maxlength","children":[]},{"level":3,"title":"[DataType]","slug":"datatype","link":"#datatype","children":[]},{"level":3,"title":"[ForeignKey]","slug":"foreignkey","link":"#foreignkey","children":[]},{"level":3,"title":"[InverseProperty]","slug":"inverseproperty","link":"#inverseproperty","children":[]},{"level":3,"title":"[DatabaseGenerated]","slug":"databasegenerated","link":"#databasegenerated","children":[]},{"level":3,"title":"[NotMapped]","slug":"notmapped","link":"#notmapped","children":[]}]}],"git":{"updatedTime":1680124405000},"filePathRelative":"modeling/model-components/attributes.md"}');export{e as data}; diff --git a/assets/back-to-top.8efcbe56.svg b/assets/back-to-top.8efcbe56.svg new file mode 100644 index 000000000..83236781a --- /dev/null +++ b/assets/back-to-top.8efcbe56.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/behaviors.html.392a1226.js b/assets/behaviors.html.392a1226.js new file mode 100644 index 000000000..fc919bb34 --- /dev/null +++ b/assets/behaviors.html.392a1226.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ada1a0e0","path":"/modeling/model-components/behaviors.html","title":"Behaviors","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Defining Behaviors","slug":"defining-behaviors","link":"#defining-behaviors","children":[{"level":3,"title":"Dependency Injection","slug":"dependency-injection","link":"#dependency-injection","children":[]}]},{"level":2,"title":"Standard Behaviors","slug":"standard-behaviors","link":"#standard-behaviors","children":[{"level":3,"title":"Properties","slug":"properties","link":"#properties","children":[]},{"level":3,"title":"Method Overview","slug":"method-overview","link":"#method-overview","children":[]},{"level":3,"title":"Method Details","slug":"method-details","link":"#method-details","children":[]}]},{"level":2,"title":"Globally Replacing the Standard Behaviors","slug":"globally-replacing-the-standard-behaviors","link":"#globally-replacing-the-standard-behaviors","children":[]}],"git":{"updatedTime":1680124405000},"filePathRelative":"modeling/model-components/behaviors.md"}');export{e as data}; diff --git a/assets/behaviors.html.9f165464.js b/assets/behaviors.html.9f165464.js new file mode 100644 index 000000000..2697f9554 --- /dev/null +++ b/assets/behaviors.html.9f165464.js @@ -0,0 +1,74 @@ +import{_ as i,y as c,z as d,X as e,B as s,Q as a,$ as t,a5 as r,P as p}from"./framework.fe9a73df.js";const D={},h=e("h1",{id:"behaviors",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#behaviors","aria-hidden":"true"},"#"),s(" Behaviors")],-1),y=e("p",null,[e("em",null,"In a CRUD system, creating, updating, and deleting are considered especially different from reading. In Coalesce, the dedicated classes that perform these operations are derivatives of a special interface known as the"),s(),e("code",null,"IBehaviors"),s(". "),e("em",null,"These are their stories"),s(".")],-1),u=e("hr",null,null,-1),v=e("p",null,"Also like data sources, these functions can be easily overridden on a per-model basis, allowing complete control over the ways in which your data is mutated by the APIs that Coalesce generates. However, unlike data sources which can have as many implementations per model as you like, you can only have one set of behaviors.",-1),m={class:"table-of-contents"},b=e("h2",{id:"defining-behaviors",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#defining-behaviors","aria-hidden":"true"},"#"),s(" Defining Behaviors")],-1),C=e("p",null,[s("By default, each of your models that Coalesce exposes will utilize the standard behaviors ("),e("code",null,"IntelliTect.Coalesce.StandardBehaviors"),s(") for the out-of-the-box API endpoints that Coalesce provides. These behaviors provide a set of create, update, and delete methods for an EF Core "),e("code",null,"DbContext"),s(", as well as a plethora of virtual methods that make the "),e("code",null,"StandardBehaviors"),s(" a great base class for your custom implementations. Unlike data sources which require an annotation to override the Coalesce-provided standard class, the simple presence of an explicitly declared set of behaviors will suppress the standard behaviors.")],-1),f={class:"custom-container tip"},g=e("p",{class:"custom-container-title"},"Note",-1),T=r(`

To create your own behaviors, you simply need to define a class that implements IntelliTect.Coalesce.IBehaviors<T>. To expose your behaviors to Coalesce, either place it as a nested class of the type T that your behaviors are for, or annotate it with the [Coalesce] attribute. Of course, the easiest way to create behaviors that doesn't require you to re-engineer a great deal of logic would be to inherit from IntelliTect.Coalesce.StandardBehaviors<T, TContext>, and then override only the parts that you need.

public class Case
+{
+    public int CaseId { get; set; }
+    public int OwnerId { get; set; }
+    public bool IsDeleted { get; set; }
+    ...
+}
+
+[Coalesce]
+public class CaseBehaviors : StandardBehaviors<Case, AppDbContext>
+{
+    public CaseBehaviors(CrudContext<AppDbContext> context) : base(context) { }
+
+    public override ItemResult BeforeSave(SaveKind kind, Case oldItem, Case item)
+    {
+        // Allow admins to bypass all validation.
+        if (User.IsInRole("Admin")) return true;
+
+        if (kind == SaveKind.Update && oldItem.OwnerId != item.OwnerId)
+            return "The owner of a case may not be changed";
+
+        // This is a new item, OR its an existing item and the owner isn't being modified.
+        if (item.CreatedById != User.GetUserId())
+            return "You are not the owner of this item.";
+
+        return true;
+    }
+
+    public override ItemResult BeforeDelete(Case item) 
+        => User.IsInRole("Manager") ? true : "Unauthorized";
+
+    public override Task ExecuteDeleteAsync(Case item)
+    {
+        // Soft delete the item.
+        item.IsDeleted = true;
+        return Db.SaveChangesAsync();
+    }
+}
+

Dependency Injection

All behaviors are instantiated using dependency injection and your application's IServiceProvider. As a result, you can add whatever constructor parameters you desire to your behaviors as long as a value for them can be resolved from your application's services. The single parameter to the StandardBehaviors is resolved in this way - the CrudContext<TContext> contains the common set of objects most commonly used, including the DbContext and the ClaimsPrincipal representing the current user.

Standard Behaviors

The standard behaviors, IntelliTect.Coalesce.StandardBehaviors<T> and its EntityFramework-supporting sibling IntelliTect.Coalesce.StandardBehaviors<T, TContext>, contain a significant number of properties and methods that can be utilized and/or overridden at your leisure.

Properties

`,7),_=e("p",null,"The object passed to the constructor that contains the set of objects needed by the standard behaviors, and those that are most likely to be used in custom implementations.",-1),w=e("p",null,[s("An instance of the db context that contains a "),e("code",null,"DbSet"),s(" for the entity handled by the behaviors")],-1),E=e("p",null,"The user making the current request.",-1),I=e("p",null,"A data source that, if set, will override the data source that is used to retrieve the target of an update operation from the database. The incoming values will then be set on this retrieved object. Null by default; override by setting a value in the constructor.",-1),S=e("p",null,"A data source that, if set, will override the data source that is used to retrieve a newly-created or just-updated object from the database after a save. The retrieved object will be returned to the client. Null by default; override by setting a value in the constructor.",-1),B=e("p",null,"A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database. The retrieved object will then be deleted. Null by default; override by setting a value in the constructor.",-1),A=r(`

A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database after it has been deleted. If an object is able to be retrieved from this data source, it will be sent back to the client. This allows soft-deleted items to be returned to the client when the user is able to see them. Null by default; override by setting a value in the constructor.

Method Overview

The standard behaviors implementation contains many different methods which can be overridden in your derived class to control functionality.

These methods often call one another, so overriding one method may cause some other method to no longer be called. The hierarchy of method calls, ignoring any logic or conditions contained within, is as follows:

SaveAsync
+    DetermineSaveKindAsync
+    GetDbSet
+    ValidateDto
+    MapIncomingDto
+    BeforeSaveAsync
+        BeforeSave
+    ExecuteSaveAsync
+    AfterSave
+
+DeleteAsync
+    BeforeDeleteAsync
+        BeforeDelete
+    ExecuteDeleteAsync
+        GetDbSet
+    AfterDelete
+

Method Details

All of the methods outlined above can be overridden. A description of each of the methods is as follows:

`,7),x=e("p",null,[s("Save the given item. This is the main entry point for saving, and takes a DTO as a parameter. This method is responsible for performing mapping to your EF models and ultimately saving to your database. If it is required that you access properties from the incoming DTO in this method, a set of extension methods "),e("code",null,"GetValue"),s(" and "),e("code",null,"GetObject"),s(" are available on the DTO for accessing properties that are mapped 1:1 with your EF models.")],-1),k=e("p",null,"Given the incoming DTO on which Save has been called, examine its properties to determine if the operation is meant to be a create or an update operation. Return this distinction along with the key that was used to make the distinction.",-1),F=e("p",null,"This method is called outside of the standard data source by the base API controller to perform role-based security on saves at the controller level.",-1),j=e("p",null,[s("Returns a "),e("code",null,"DbSet"),s(" that items can be added to (creates) or remove from (deletes).")],-1),O=e("code",null,"BeforeSave",-1),P=e("code",null,"IClassDto",-1),R=r("

Map the properties of the incoming DTO to the model that will be saved to the database. For a SaveKind.Create, this will call the MapToNew method on the DTO and a new instance must be returned (item will be null). For a SaveKind.Update, this will call the MapTo method on the DTO, and the incoming item must be returned. If more precise control is needed, extension methods on IClassDto<T> or casting to a known type can be used to get specific values. If all else fails, the DTO can be reflected upon.

",1),q=e("p",null,"Provides an easy way for derived classes to intercept a save attempt and either reject it by returning an unsuccessful result, or approve it by returning success. The incoming item can also be modified at will in this method to override changes that the client made as desired.",-1),M=e("code",null,"ref T item",-1),K=e("code",null,"ref IncludeTree includeTree",-1),N=e("div",{class:"custom-container warning"},[e("p",{class:"custom-container-title"},"WARNING"),e("p",null,[s("Setting "),e("code",null,"ref T item"),s(" to null will prevent the new object from being returned - be aware that this can be harmful in create scenarios since it prevents the client from receiving the primary key of the newly created item. If autoSave is enabled on the client, this could cause a large number of duplicate objects to be created in the database, since each subsequent save by the client will be treated as a create when the incoming object lacks a primary key.")])],-1),U=e("p",null,"Deletes the given item.",-1),G=e("p",null,"Provides an easy way to intercept a delete request and potentially reject it (by returning a non-success ItemResult).",-1),V=e("p",null,[s("Performs the delete action against the database. The implementation of this method removes the item from its corresponding "),e("code",null,"DbSet"),s(", and then calls "),e("code",null,"Db.SaveChangesAsync()"),s(".")],-1),z=e("p",null,"Overriding this allows for changing this row-deletion implementation to something else, like setting of a soft delete flag, or copying the data into another archival table before deleting.",-1),Y=e("code",null,"ref T item",-1),L=e("code",null,"ref IncludeTree includeTree",-1),W=r(`

Globally Replacing the Standard Behaviors

You can, of course, create a custom base behaviors class that all your custom implementations inherit from. But, what if you want to override the standard behaviors across your entire application, so that StandardBehaviors<,> will never be instantiated? You can do that too!

Simply create a class that implements IEntityFrameworkBehaviors<,> (the StandardBehaviors<,> already does - feel free to inherit from it), then register it at application startup like so:

public class MyBehaviors<T, TContext> : StandardBehaviors<T, TContext>
+    where T : class
+    where TContext : DbContext
+{
+    public MyBehaviors(CrudContext<TContext> context) : base(context)
+    {
+    }
+
+    ...
+}
+
public void ConfigureServices(IServiceCollection services)
+{
+    services.AddCoalesce(b =>
+    {
+        b.AddContext<AppDbContext>();
+        b.UseDefaultBehaviors(typeof(MyBehaviors<,>));
+    });
+

Your custom behaviors class must have the same generic type parameters - <T, TContext>. Otherwise, the Microsoft.Extensions.DependencyInjection service provider won't know how to inject it.

`,6);function H(Q,X){const o=p("RouterLink"),l=p("router-link"),n=p("Prop");return c(),d("div",null,[h,y,u,e("p",null,[s("Coalesce separates out the parts of your API that read your data from the parts that mutate it. The read portion is performed by "),a(o,{to:"/modeling/model-components/data-sources.html"},{default:t(()=>[s("Data Sources")]),_:1}),s(", and the mutations are performed by behaviors. Like data sources, there exists a standard set of behaviors that Coalesce provides out-of-the-box that cover the most common use cases for creating, updating, and deleting objects in your data model.")]),v,e("nav",m,[e("ul",null,[e("li",null,[a(l,{to:"#defining-behaviors"},{default:t(()=>[s("Defining Behaviors")]),_:1}),e("ul",null,[e("li",null,[a(l,{to:"#dependency-injection"},{default:t(()=>[s("Dependency Injection")]),_:1})])])]),e("li",null,[a(l,{to:"#standard-behaviors"},{default:t(()=>[s("Standard Behaviors")]),_:1}),e("ul",null,[e("li",null,[a(l,{to:"#properties"},{default:t(()=>[s("Properties")]),_:1})]),e("li",null,[a(l,{to:"#method-overview"},{default:t(()=>[s("Method Overview")]),_:1})]),e("li",null,[a(l,{to:"#method-details"},{default:t(()=>[s("Method Details")]),_:1})])])]),e("li",null,[a(l,{to:"#globally-replacing-the-standard-behaviors"},{default:t(()=>[s("Globally Replacing the Standard Behaviors")]),_:1})])])]),b,C,e("div",f,[g,e("p",null,[s("When you define a set of custom behaviors, take note that these are only used by the standard set of API endpoints that Coalesce always provides. They will not be used to handle any mutations in any "),a(o,{to:"/modeling/model-components/methods.html"},{default:t(()=>[s("Methods")]),_:1}),s(" you write for your models.")])]),T,a(n,{def:"CrudContext Context"}),_,a(n,{def:"TContext Db"}),w,a(n,{def:"ClaimsPrincipal User"}),E,a(n,{def:"IDataSource OverrideFetchForUpdateDataSource"}),I,a(n,{def:"IDataSource OverridePostSaveResultDataSource"}),S,a(n,{def:"IDataSource OverrideFetchForDeleteDataSource"}),B,a(n,{def:"IDataSource OverridePostDeleteResultDataSource"}),A,a(n,{def:"Task> SaveAsync(TDto incomingDto, IDataSource dataSource, IDataSourceParameters parameters)"}),x,a(n,{def:"Task<(SaveKind Kind, object? IncomingKey)> DetermineSaveKindAsync(TDto incomingDto, IDataSource dataSource, IDataSourceParameters parameters)"}),k,F,a(n,{def:"DbSet GetDbSet()"}),j,a(n,{def:"ItemResult ValidateDto(SaveKind kind, IClassDto dto)"}),e("p",null,[s("Provides a chance to validate the properties of the DTO object itself, as opposed to doing validation in "),O,s(" of the properties of the model after the DTO has been mapped to the model. This also where "),a(o,{to:"/topics/security.html#validateattributesforsaves"},{default:t(()=>[s("attribute-based validation")]),_:1}),s(" is performed.")]),e("p",null,[s("To perform custom validation in this method (uncommon), there are a number of extension methods on "),P,s(" that can be used to access the value of the properties of "),a(o,{to:"/stacks/agnostic/dtos.html"},{default:t(()=>[s("Generated C# DTOs")]),_:1}),s(". For behaviors on "),a(o,{to:"/modeling/model-types/dtos.html"},{default:t(()=>[s("Custom DTOs")]),_:1}),s(" where the DTO type is known, simply cast to the correct type.")]),a(n,{def:"T MapIncomingDto(SaveKind kind, T? item, TDto dto, IDataSourceParameters parameters)"}),R,a(n,{def:`Task BeforeSaveAsync(SaveKind kind, T? oldItem, T item); +ItemResult BeforeSave(SaveKind kind, T? oldItem, T item)`}),q,a(n,{def:"ItemResult AfterSave(SaveKind kind, T? oldItem, ref T item, ref IncludeTree? includeTree)"}),e("p",null,[s("Provides an easy way for derived classes to perform actions after a save operation has been completed. Failure results returned here will present an error to the client, but will not prevent modifications to the database since changes have already been saved at this point. This method can optionally modify or replace the item that is sent back to the client after a save by setting "),M,s(" to another object or to null. Setting "),K,s(" will override the "),a(o,{to:"/concepts/include-tree.html"},{default:t(()=>[s("Include Tree")]),_:1}),s(" used to shape the response object.")]),N,a(n,{def:"Task> DeleteAsync(object id, IDataSource dataSource, IDataSourceParameters parameters)"}),U,a(n,{def:`Task BeforeDeleteAsync(T item); +ItemResult BeforeDelete(T item)`}),G,a(n,{def:"Task ExecuteDeleteAsync(T item)"}),V,z,a(n,{def:"void AfterDelete(ref T item, ref IncludeTree? includeTree)"}),e("p",null,[s("Allows for performing any sort of cleanup actions after a delete has completed. If the item was still able to be retrieved from the database after the delete operation completed, this method allows lets you modify or replace the item that is sent back to the client by setting "),Y,s(" to another object or to null. Setting "),L,s(" will override the "),a(o,{to:"/concepts/include-tree.html"},{default:t(()=>[s("Include Tree")]),_:1}),s(" used to shape the response object.")]),W])}const J=i(D,[["render",H],["__file","behaviors.html.vue"]]);export{J as default}; diff --git a/assets/bindings.html.3ecb74ed.js b/assets/bindings.html.3ecb74ed.js new file mode 100644 index 000000000..fc56be059 --- /dev/null +++ b/assets/bindings.html.3ecb74ed.js @@ -0,0 +1,54 @@ +import{_ as c,y as d,z as u,X as e,Q as t,$ as n,B as s,a5 as a,P as r}from"./framework.fe9a73df.js";const h={},m=e("h1",{id:"knockout-bindings",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#knockout-bindings","aria-hidden":"true"},"#"),s(" Knockout Bindings")],-1),y=e("p",null,"Coalesce provides a number of knockout bindings that make common model binding activities much easier.",-1),b=e("p",null,[s("Editors Note: On this page, some bindings are split into their requisite HTML component with their "),e("code",null,"data-bind"),s(" component listed immediately after. Keep this in mind when reading.")],-1),D={class:"table-of-contents"},f=a(`

Input Bindings

select2Ajax

<select data-bind="
+    select2Ajax: personId, 
+    url: '/api/Person/list', 
+    idField: 'personId', 
+    textField: 'Name', 
+    object: person, 
+    allowClear: true
+"></select>
+

Creates a select2 dropdown using the specified url and fields that can be used to select an object from the endpoint specified. Additional complimentary bindings include:

`,4),g=e("p",null,"The Coalesce List API url to call to populate the contents of the dropdown.",-1),v=e("p",null,[s("The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set on the observable specified for the main "),e("code",null,"select2Ajax"),s(" binding.")],-1),C=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.",-1),_=e("p",null,[s("An observable that holds the full object corresponding to the foreign key property being bound to. If the selected value changes, this will be set to null to avoid representation of incorrect data (unless "),e("code",null,"setObject"),s(" is used - see below).")],-1),x=a("

If true, the observable specified by the object binding will be set to the selected data when an option is chosen in the dropdown. Binding itemViewModel is required if this binding is set.

Additionally, requests to the API to populate the dropdown will request the entire object, as opposed to only the two fields specified for idField and textField like is normally done when this binding is missing or set to false. To override this behavior and continue requesting only specific fields even when setObject is true, add fields=field1,field2,... to the query string of the url binding.

",2),w=e("p",null,[s("A reference to the class that represents the type of the object held in the "),e("code",null,"object"),s(" observable. This is used when constructing new objects from the results of the API call. Not used if "),e("code",null,"setObject"),s(" is false or unspecified. For example, "),e("code",null,"setObject: true, itemViewModel: ViewModels.Person"),s(".")],-1),k=e("p",null,"The number of items to request in each call to the server.",-1),E=e("p",null,[s("A string containing the substring "),e("code",null,"{0}"),s(", which will be replaced with the text value of an option in the dropdown list when the option is displayed.")],-1),j=e("p",null,[s("A string containing the substring "),e("code",null,"{0}"),s(", which will be replaced with the text value of the selected option of the dropdown list.")],-1),A=e("p",null,"If true, a cache-busting querystring parameter will be included in AJAX requests.",-1),q=e("p",null,[s("Directly maps to select2 option "),e("code",null,"selectOnClose"),s(".")],-1),F=e("p",null,[s("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),s(".")],-1),T=e("p",null,[s("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),s(".")],-1),I=a(`

If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

select2AjaxMultiple

<select multiple="multiple" data-bind="
+    select2AjaxMultiple: people, 
+    url: '/api/Person/list', 
+    idField: 'personId', 
+    textField: 'Name', 
+    itemViewModel: ViewModels.PersonCase
+"></select>
+

Creates a select2 multi-select input for choosing objects that participate as the foreign object in a many-to-many relationship with the current object. The primary select2AjaxMultiple binding takes the collection of items that make up the foreign side of the relationship. This is NOT the collection of the join objects (a.k.a. middle table objects) in the relationship.

Additional complimentary bindings include:

`,5),M=e("p",null,[s("The Coalesce List API url to call to populate the contents of the dropdown. In order to only receive specific fields from the server, add "),e("code",null,"fields=field1,field2,..."),s(" to the query string of the url, ensuring that at least the "),e("code",null,"idField"),s(" and "),e("code",null,"textField"),s(" are included in that collection.")],-1),O=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set as the key of the foreign object in the many-to-many relationship.",-1),B=e("p",null,"The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.",-1),P=e("p",null,[s("A reference to the class that represents the types in the supplied collection. For example, a many-to-many between "),e("code",null,"Person"),s(" and "),e("code",null,"Case"),s(" objects where "),e("code",null,"Case"),s(" is the object being bound to and "),e("code",null,"Person"),s(" is the type represented by a child collection, the correct value is "),e("code",null,"ViewModels.Person"),s(". This is used when constructing new objects representing the relationship when a new item is selected.")],-1),V=e("p",null,"The number of items to request in each call to the server.",-1),S=e("p",null,[s("A string containing the substring "),e("code",null,"{0}"),s(", which will be replaced with the text value of an option in the dropdown list when the option is displayed.")],-1),N=e("p",null,[s("A string containing the substring "),e("code",null,"{0}"),s(", which will be replaced with the text value of the selected option of the dropdown list.")],-1),K=e("p",null,"If true, a cache-busting querystring parameter will be included in AJAX requests.",-1),Y=e("p",null,[s("Directly maps to select2 option "),e("code",null,"selectOnClose"),s(".")],-1),z=e("p",null,[s("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),s(".")],-1),L=e("p",null,[s("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),s(".")],-1),W=a(`

If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

select2AjaxText

<select data-bind="
+    select2AjaxText: schoolName, 
+    url: '/api/Person/SchoolNames'
+"></select>
+

Creates a select2 dropdown against the specified url where the url returns a collection of string values that are potential selection candidates. The dropdown also allows the user to input any value they choose - the API simply serves suggested values.

`,4),X=a("

The url to call to populate the contents of the dropdown. This should be an endpoint that returns one of the following:

  • A raw string[]
  • An object that conforms to { list: string[] }
  • An object that conforms to { object: string[] }
  • An object that conforms to { list: { [prop: string]: string } } where the value given to resultField is a valid property of the returned objects.
  • An object that conforms to { object: { [prop: string]: string } } where the value given to resultField is a valid property of the returned objects.

The url will also be passed a search parameter and a page parameter appended to the query string. The chosen endpoint is responsible for implementing this functionality. Page size is expected to be some fixed value. Implementer should anticipate that the requested page may be out of range.

The cases listed above that accept arrays of objects (as opposed to arrays of strings) require that the resultField binding is also used. These are designed for obtaining string values from objects obtained from the standard list endpoint.

",4),$=e("p",null,[s("If provided, specifies a field on the objects returned from the API to pull the string values from. See examples in "),e("code",null,"url"),s(" above.")],-1),J=e("p",null,[s("If "),e("code",null,"false"),s(", the user's search input will not be presented as a valid selectable value; only the exact values obtained from the API endpoint will be selectable.")],-1),H=e("p",null,[s("Directly maps to select2 option "),e("code",null,"selectOnClose"),s(".")],-1),Q=e("p",null,[s("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),s(".")],-1),R=e("p",null,[s("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),s(".")],-1),U=a(`

If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

select2

<select data-bind="select2: selectedNumber">
+    <option value="1">Option 1</option>
+    <option value="2">Option 2</option>
+</select>
+
`,3),Z={href:"https://knockoutjs.com/",target:"_blank",rel:"noopener noreferrer"},G=e("p",null,[s("Directly maps to select2 option "),e("code",null,"selectOnClose"),s(".")],-1),ee=e("p",null,[s("Whether or not to allow the current select to be set to null. Directly maps to select2 option "),e("code",null,"allowClear"),s(".")],-1),se=e("p",null,[s("Placeholder when nothing is selected. Directly maps to select2 option "),e("code",null,"placeholder"),s(".")],-1),te=a(`

If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

datePicker

<div class="input-group date">
+    <input data-bind="datePicker: birthDate" type="text" class="form-control" id-prefix="datePicker" />
+    <span class="input-group-addon">
+        <span class="fa fa-calendar"></span>
+    </span>
+</div>
+
`,3),le=e("code",null,"moment.Moment",-1),ne={href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noopener noreferrer"},oe=e("p",null,[s("If true, the date portion of the "),e("code",null,"moment.Moment"),s(" object will be preserved by the date picker. Only the time portion will be changed by user input.")],-1),ae=e("p",null,[s("If true, the time portion of the "),e("code",null,"moment.Moment"),s(" object will be preserved by the date picker. Only the date portion will be changed by user input.")],-1),ie=e("code",null,"M/D/YY h:mm a",-1),pe={href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noopener noreferrer"},re={href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noopener noreferrer"},ce={href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noopener noreferrer"},de={href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noopener noreferrer"},ue={href:"https://www.npmjs.com/package/eonasdan-bootstrap-datetimepicker",target:"_blank",rel:"noopener noreferrer"},he=a(`

If true, the datePicker will update the underlying observable on each input change. Otherwise, the observable will only be changed when the datePicker loses focus (on blur).

saveImmediately

<div data-bind="with: product">
+    <input type="text" data-bind="textValue: description, saveImmediately: true" />
+</div>
+
`,3),me=e("code",null,"$data",-1),ye=e("code",null,"Coalesce.BaseViewModel",-1),be=e("code",null,"saveTimeoutMs",-1),De=e("code",null,"0",-1),fe=a(`

delaySave

<div data-bind="with: product">
+    <input type="text" data-bind="textValue: description, delaySave: true" />
+</div>
+
`,2),ge=e("code",null,"$data",-1),ve=e("code",null,"Coalesce.BaseViewModel",-1),Ce=e("code",null,"autoSaveEnabled",-1),_e=e("code",null,"false",-1),xe=e("code",null,"autoSaveEnabled",-1),we=a(`

Display Bindings

tooltip

<div data-bind="tooltip: tooltipText">Some Element</div>
+<div data-bind="tooltip: {title: note, placement: 'bottom', animation: false}">Some Element</div>
+
`,3),ke={href:"https://getbootstrap.com/docs/3.3/javascript/#tooltips",target:"_blank",rel:"noopener noreferrer"},Ee=a(`

fadeVisible

<div data-bind="fadeVisible: isVisible">Some Element</div>
+

Similar to the Knockout visible binding, but uses jQuery fadeIn/fadeOut calls to perform the transition.

slideVisible

<div data-bind="slideVisible: isVisible">Some Element</div>
+

Similar to the Knockout visible, but uses jQuery slideIn/slideOut calls to perform the transition.

moment

<div data-bind="moment: momentObservable"></div>
+<div data-bind="moment: momentObservable, format: 'MM/DD/YYYY hh:mm a'"></div>
+

Controls the text of the element by calling the format method on a moment object.

momentFromNow

<div data-bind="momentFromNow: momentObservable"></div>
+<div data-bind="momentFromNow: momentObservable, shorten: true"></div>
+

Controls the text of the element by calling the fromNow method on a moment object. If shorten is true, certain phrases will be slightly shortened.

Utility Bindings

let

<div class="item">
+    <!-- ko let: { showControls: $data.isEditing() || $parent.editingChildren() } -->
+    <button data-bind="click: $root.editItem, visible: showControls">Edit</button>
+    <span data-bind="text: name"></span>
+    <button data-bind="click: $root.deleteItem, visible: showControls">Delete</button>
+    <!-- /ko -->
+</div>
+

The let binding is a somewhat common construct used in Knockout applications, but isn't part of Knockout itself. It effectively allows the creation of variables in the binding context, allowing complex statements which may be used multiple times to be aliased for both clarity of code and better performance.

Knockout Binding Defaults

Knockout Helpers

These are static properties on IntelliTect.Coalesce.Knockout.Helpers.Knockout you can assign to somewhere in the app lifecycle startup to change the default markup generated server-side when using @Knockout.* methods to render Knockout bindings in your .cshtml files.

`,19),je=e("p",null,"The default number of Bootstrap grid columns a field label should span across.",-1),Ae=e("p",null,"The default number of Bootstrap grid columns a form input should span across.",-1),qe=e("code",null,"DateTimeOffset",-1),Fe={class:"custom-container tip"},Te=e("p",{class:"custom-container-title"},"Note",-1),Ie=e("code",null,"DefaultDateFormat",-1),Me=e("code",null,"DefaultTimeFormat",-1),Oe=e("code",null,"DefaultDateTimeFormat",-1),Be={href:"https://momentjs.com/docs/#/displaying/format/",target:"_blank",rel:"noopener noreferrer"},Pe=a(`

Timezone

The date/time picker properties can be coupled with DateTimeOffset model properties to display time values localized for the current user's locale. If you want to make the localization static, simply include a script block in your _Layout.cshtml or in a specific view that sets the default for Moment.js:

<script>
+moment.tz.setDefault("America/Chicago");
+</script>
+

Note

This needs to happen after Moment is loaded, but before the bootstrap-datetimepicker script is loaded.

`,4);function Ve(Se,Ne){const o=r("router-link"),l=r("Prop"),i=r("ExternalLinkIcon"),p=r("RouterLink");return d(),u("div",null,[m,y,b,e("nav",D,[e("ul",null,[e("li",null,[t(o,{to:"#input-bindings"},{default:n(()=>[s("Input Bindings")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#select2ajax"},{default:n(()=>[s("select2Ajax")]),_:1})]),e("li",null,[t(o,{to:"#select2ajaxmultiple"},{default:n(()=>[s("select2AjaxMultiple")]),_:1})]),e("li",null,[t(o,{to:"#select2ajaxtext"},{default:n(()=>[s("select2AjaxText")]),_:1})]),e("li",null,[t(o,{to:"#select2"},{default:n(()=>[s("select2")]),_:1})]),e("li",null,[t(o,{to:"#datepicker"},{default:n(()=>[s("datePicker")]),_:1})]),e("li",null,[t(o,{to:"#saveimmediately"},{default:n(()=>[s("saveImmediately")]),_:1})]),e("li",null,[t(o,{to:"#delaysave"},{default:n(()=>[s("delaySave")]),_:1})])])]),e("li",null,[t(o,{to:"#display-bindings"},{default:n(()=>[s("Display Bindings")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#tooltip"},{default:n(()=>[s("tooltip")]),_:1})]),e("li",null,[t(o,{to:"#fadevisible"},{default:n(()=>[s("fadeVisible")]),_:1})]),e("li",null,[t(o,{to:"#slidevisible"},{default:n(()=>[s("slideVisible")]),_:1})]),e("li",null,[t(o,{to:"#moment"},{default:n(()=>[s("moment")]),_:1})]),e("li",null,[t(o,{to:"#momentfromnow"},{default:n(()=>[s("momentFromNow")]),_:1})])])]),e("li",null,[t(o,{to:"#utility-bindings"},{default:n(()=>[s("Utility Bindings")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#let"},{default:n(()=>[s("let")]),_:1})])])]),e("li",null,[t(o,{to:"#knockout-binding-defaults"},{default:n(()=>[s("Knockout Binding Defaults")]),_:1}),e("ul",null,[e("li",null,[t(o,{to:"#knockout-helpers"},{default:n(()=>[s("Knockout Helpers")]),_:1})]),e("li",null,[t(o,{to:"#timezone"},{default:n(()=>[s("Timezone")]),_:1})])])])])]),f,t(l,{def:"url: string",lang:"ts","id-prefix":"select2Ajax"}),g,t(l,{def:"idField: string",lang:"ts","id-prefix":"select2Ajax"}),v,t(l,{def:"textField: string",lang:"ts","id-prefix":"select2Ajax"}),C,t(l,{def:"object?: KnockoutObservable",lang:"ts","id-prefix":"select2Ajax"}),_,t(l,{def:"setObject: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),x,t(l,{def:"itemViewModel?: (new (newItem: object) => Coalesce.BaseViewModel)",lang:"ts","id-prefix":"select2Ajax"}),w,t(l,{def:"pageSize: number = 25",lang:"ts","id-prefix":"select2Ajax"}),k,t(l,{def:"format: string = '{0}'",lang:"ts","id-prefix":"select2Ajax"}),E,t(l,{def:"selectionFormat: string = '{0}'",lang:"ts","id-prefix":"select2Ajax"}),j,t(l,{def:"cache: boolean = true",lang:"ts","id-prefix":"select2Ajax"}),A,t(l,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),q,t(l,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2Ajax"}),F,t(l,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2Ajax"}),T,t(l,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2Ajax"}),I,t(l,{def:"url: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),M,t(l,{def:"idField: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),O,t(l,{def:"textField: string",lang:"ts","id-prefix":"select2AjaxMultiple"}),B,t(l,{def:"itemViewModel: (new (newItem: object) => Coalesce.BaseViewModel)",lang:"ts","id-prefix":"select2AjaxMultiple"}),P,t(l,{def:"pageSize: number = 25",lang:"ts","id-prefix":"select2AjaxMultiple"}),V,t(l,{def:"format: string = '{0}'",lang:"ts","id-prefix":"select2AjaxMultiple"}),S,t(l,{def:"selectionFormat: string = '{0}'",lang:"ts","id-prefix":"select2AjaxMultiple"}),N,t(l,{def:"cache: boolean = true",lang:"ts","id-prefix":"select2AjaxMultiple"}),K,t(l,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2AjaxMultiple"}),Y,t(l,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2AjaxMultiple"}),z,t(l,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2AjaxMultiple"}),L,t(l,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2AjaxMultiple"}),W,t(l,{def:"url: string",lang:"ts","id-prefix":"select2AjaxText"}),X,t(l,{def:"resultField?: string",lang:"ts","id-prefix":"select2AjaxText"}),$,t(l,{def:"allowCustom: boolean = true",lang:"ts","id-prefix":"select2AjaxText"}),J,t(l,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2AjaxText"}),H,t(l,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2AjaxText"}),Q,t(l,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2AjaxText"}),R,t(l,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2AjaxText"}),U,e("p",null,[s("Sets up a basic select2 dropdown on an HTML select element. Dropdown contents should be populated through other means - either using stock "),e("a",Z,[s("Knockout"),t(i)]),s(" bindings or server-side static contents (via cshtml).")]),t(l,{def:"selectOnClose: boolean = false",lang:"ts","id-prefix":"select2"}),G,t(l,{def:"allowClear: boolean = true",lang:"ts","id-prefix":"select2"}),ee,t(l,{def:"placeholder: string = 'select'",lang:"ts","id-prefix":"select2"}),se,t(l,{def:"openOnFocus: boolean = false",lang:"ts","id-prefix":"select2"}),te,e("p",null,[s("Creates a date/time picker for changing a "),le,s(" property. The control used is "),e("a",ne,[s("bootstrap-datetimepicker"),t(i)])]),t(l,{def:"preserveDate: boolean = false",lang:"ts","id-prefix":"date-picker"}),oe,t(l,{def:"preserveTime: boolean = false",lang:"ts","id-prefix":"date-picker"}),ae,t(l,{def:"format: string = 'M/D/YY h:mm a'",lang:"ts","id-prefix":"date-picker"}),e("p",null,[s("Specify the moment-compatible format string to be used as the display format for the text value shown on the date picker. Defaults to "),ie,s(". Direct pass-through to "),e("a",pe,[s("bootstrap-datetimepicker"),t(i)]),s(".")]),t(l,{def:"sideBySide: boolean = false",lang:"ts","id-prefix":"date-picker"}),e("p",null,[s("If true, places the time picker next to the date picker, visible at the same time. Direct pass-through to corresponding "),e("a",re,[s("bootstrap-datetimepicker"),t(i)]),s(" option.")]),t(l,{def:"stepping: number = 1",lang:"ts","id-prefix":"date-picker"}),e("p",null,[s("Direct pass-through to corresponding "),e("a",ce,[s("bootstrap-datetimepicker"),t(i)]),s(" option.")]),t(l,{def:"timeZone: string = ''",lang:"ts","id-prefix":"date-picker"}),e("p",null,[s("Direct pass-through to corresponding "),e("a",de,[s("bootstrap-datetimepicker"),t(i)]),s(" option.")]),t(l,{def:"keyBinds = { left: null, right: null, delete: null }",lang:"ts","id-prefix":"date-picker"}),e("p",null,[s("Override key bindings of the date picker. Direct pass-through to corresponding "),e("a",ue,[s("bootstrap-datetimepicker"),t(i)]),s(" option.")]),t(l,{def:"updateImmediate: boolean = false",lang:"ts","id-prefix":"date-picker"}),he,e("p",null,[s("When used in a context where "),me,s(" is a "),ye,s(", that object's "),be,s(" configuration property (see "),t(p,{to:"/stacks/ko/client/model-config.html"},{default:n(()=>[s("ViewModel Configuration")]),_:1}),s(") will be set to "),De,s(" when the element it is placed on gains focus. This value will be reverted to its previous value when the element loses focus. This will cause any changes to the object, including any observable bound as input on the element, to trigger a save immediately rather than after a delay (defaults to 500ms).")]),fe,e("p",null,[s("When used in a context where "),ge,s(" is a "),ve,s(", that object's "),Ce,s(" configuration property (see "),t(p,{to:"/stacks/ko/client/model-config.html"},{default:n(()=>[s("ViewModel Configuration")]),_:1}),s(") will be set to "),_e,s(" when the element it is placed on gains focus. This will cause any changes to the object, including any observable bound as input on the element, to not trigger auto saves while the element has focus. When the element loses focus, the "),xe,s(" flag will be reverted to its previous value and an attempt will be made to save the object.")]),we,e("p",null,[s("Wrapper around the "),e("a",ke,[s("Bootstrap tooltip component"),t(i)]),s(". Binding can either be simply a string (or observable string), or it can be an object that will be passed directly to the Bootstrap tooltip component.")]),Ee,t(l,{def:"public static int DefaultLabelCols { get; set; } = 3;"}),je,t(l,{def:"public static int DefaultInputCols { get; set; } = 9;"}),Ae,t(l,{def:'public static string DefaultDateFormat { get; set; } = "M/D/YYYY";'}),e("p",null,[s("Sets the default date-only format to be used by all date/time pickers. This only applies to models with a date-only "),t(p,{to:"/modeling/model-components/attributes/date-type.html"},{default:n(()=>[s("[DateType]")]),_:1}),s(" attribute.")]),t(l,{def:'public static string DefaultTimeFormat { get; set; } = "h:mm a";'}),e("p",null,[s("Sets the default time-only format to be used by all date/time pickers. This only applies to models with a time-only "),t(p,{to:"/modeling/model-components/attributes/date-type.html"},{default:n(()=>[s("[DateType]")]),_:1}),s(" attribute.")]),t(l,{def:'public static string DefaultDateTimeFormat { get; set; } = "M/D/YYYY h:mm a";'}),e("p",null,[s("Sets the default date/time format to be used by all date/time pickers. This only applies to "),qe,s(" model properties that do not have a limiting "),t(p,{to:"/modeling/model-components/attributes/date-type.html"},{default:n(()=>[s("[DateType]")]),_:1}),s(" attribute.")]),e("div",Fe,[Te,e("p",null,[Ie,s(", "),Me,s(" and "),Oe,s(" all take various formatting strings from the Moment.js library. A full listing can be found on the "),e("a",Be,[s("Moment website"),t(i)]),s(".")])]),Pe])}const Ye=c(h,[["render",Ve],["__file","bindings.html.vue"]]);export{Ye as default}; diff --git a/assets/bindings.html.5a16a120.js b/assets/bindings.html.5a16a120.js new file mode 100644 index 000000000..80590f7dc --- /dev/null +++ b/assets/bindings.html.5a16a120.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-024b84b2","path":"/stacks/ko/client/bindings.html","title":"Knockout Bindings","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Input Bindings","slug":"input-bindings","link":"#input-bindings","children":[{"level":3,"title":"select2Ajax","slug":"select2ajax","link":"#select2ajax","children":[]},{"level":3,"title":"select2AjaxMultiple","slug":"select2ajaxmultiple","link":"#select2ajaxmultiple","children":[]},{"level":3,"title":"select2AjaxText","slug":"select2ajaxtext","link":"#select2ajaxtext","children":[]},{"level":3,"title":"select2","slug":"select2","link":"#select2","children":[]},{"level":3,"title":"datePicker","slug":"datepicker","link":"#datepicker","children":[]},{"level":3,"title":"saveImmediately","slug":"saveimmediately","link":"#saveimmediately","children":[]},{"level":3,"title":"delaySave","slug":"delaysave","link":"#delaysave","children":[]}]},{"level":2,"title":"Display Bindings","slug":"display-bindings","link":"#display-bindings","children":[{"level":3,"title":"tooltip","slug":"tooltip","link":"#tooltip","children":[]},{"level":3,"title":"fadeVisible","slug":"fadevisible","link":"#fadevisible","children":[]},{"level":3,"title":"slideVisible","slug":"slidevisible","link":"#slidevisible","children":[]},{"level":3,"title":"moment","slug":"moment","link":"#moment","children":[]},{"level":3,"title":"momentFromNow","slug":"momentfromnow","link":"#momentfromnow","children":[]}]},{"level":2,"title":"Utility Bindings","slug":"utility-bindings","link":"#utility-bindings","children":[{"level":3,"title":"let","slug":"let","link":"#let","children":[]}]},{"level":2,"title":"Knockout Binding Defaults","slug":"knockout-binding-defaults","link":"#knockout-binding-defaults","children":[{"level":3,"title":"Knockout Helpers","slug":"knockout-helpers","link":"#knockout-helpers","children":[]},{"level":3,"title":"Timezone","slug":"timezone","link":"#timezone","children":[]}]}],"git":{"updatedTime":1663268139000},"filePathRelative":"stacks/ko/client/bindings.md"}');export{e as data}; diff --git a/assets/c-admin-display.html.30c2d887.js b/assets/c-admin-display.html.30c2d887.js new file mode 100644 index 000000000..a84b3073b --- /dev/null +++ b/assets/c-admin-display.html.30c2d887.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-131ad0c4","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html","title":"c-admin-display","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Slots","slug":"slots","link":"#slots","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-display.md"}');export{e as data}; diff --git a/assets/c-admin-display.html.a866e748.js b/assets/c-admin-display.html.a866e748.js new file mode 100644 index 000000000..23ab95c7d --- /dev/null +++ b/assets/c-admin-display.html.a866e748.js @@ -0,0 +1,9 @@ +import{_ as p,y as c,z as i,W as l,X as s,B as e,Q as a,$ as t,a5 as d,P as r}from"./framework.fe9a73df.js";const u={},m=s("h1",{id:"c-admin-display",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-admin-display","aria-hidden":"true"},"#"),e(" c-admin-display")],-1),y={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},D=s("code",null,"coalesce-admin-list",-1),h=s("code",null,"type",-1),v=s("code",null,"filter.",-1),_={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},f=s("code",null,"coalesce-admin-item",-1),g=s("code",null,"type",-1),C=s("code",null,"id",-1),b=d(`

Examples

<!-- Renders regularly as text: -->
+<c-admin-display :model="person" for="firstName" />
+
+<!-- Renders as a link to an item: -->
+<c-admin-display :model="person" for="company" />
+
+<!-- Renders as a link to a list: -->
+<c-admin-display :model="person" for="casesAssigned" />
+

Props

`,3),k=s("h2",{id:"slots",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#slots","aria-hidden":"true"},"#"),e(" Slots")],-1);function E(x,q){const n=r("RouterLink"),o=r("ExternalLinkIcon");return c(),i("div",null,[m,l(" MARKER:summary "),s("p",null,[e("Behaves the same as "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:t(()=>[e("c-display")]),_:1}),e(", except any collection navigation properties will be rendered as links to an admin list page, and any models will be rendered as a link to an admin item page.")]),l(" MARKER:summary-end "),s("p",null,[e("Links for collections are resolved from "),s("a",y,[e("vue-router"),a(o)]),e(" with a route name of "),D,e(", a "),h,e(" route param containing the name of the collection's type, and a query parameter "),v,e(" with a value of the primary key of the owner of the collection. This route is expected to resolve to a "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html"},{default:t(()=>[e("c-admin-table-page")]),_:1}),e(", which is setup by default by the template outlined in "),a(n,{to:"/stacks/vue/getting-started.html"},{default:t(()=>[e("Getting Started with Vue")]),_:1}),e(".")]),s("p",null,[e("Links for single models are resolved from "),s("a",_,[e("vue-router"),a(o)]),e(" with a route name of "),f,e(", a "),g,e(" route param containing the name of the model's type, and a "),C,e(" route param containing the object's primary key. This route is expected to resolve to a "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html"},{default:t(()=>[e("c-admin-editor-page")]),_:1}),e(", which is setup by default by the template outlined in "),a(n,{to:"/stacks/vue/getting-started.html"},{default:t(()=>[e("Getting Started with Vue")]),_:1}),e(".")]),b,s("p",null,[e("Same as "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:t(()=>[e("c-display")]),_:1}),e(".")]),k,s("p",null,[e("Same as "),a(n,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:t(()=>[e("c-display")]),_:1}),e(".")])])}const F=p(u,[["render",E],["__file","c-admin-display.html.vue"]]);export{F as default}; diff --git a/assets/c-admin-editor-page.html.179cc0d1.js b/assets/c-admin-editor-page.html.179cc0d1.js new file mode 100644 index 000000000..a6bdb9631 --- /dev/null +++ b/assets/c-admin-editor-page.html.179cc0d1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-37e06bde","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html","title":"c-admin-editor-page","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.md"}');export{e as data}; diff --git a/assets/c-admin-editor-page.html.bf671050.js b/assets/c-admin-editor-page.html.bf671050.js new file mode 100644 index 000000000..1c0e05a1b --- /dev/null +++ b/assets/c-admin-editor-page.html.bf671050.js @@ -0,0 +1,20 @@ +import{_ as c,y as i,z as d,W as t,X as n,B as s,Q as e,$ as l,a5 as D,P as o}from"./framework.fe9a73df.js";const y={},m=n("h1",{id:"c-admin-editor-page",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#c-admin-editor-page","aria-hidden":"true"},"#"),s(" c-admin-editor-page")],-1),u={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},v=D(`

Examples

// router.ts or main.ts
+
+// WITHOUT Vuetify A la carte:
+import { CAdminEditorPage } from 'coalesce-vue-vuetify';
+// WITH Vuetify A-la-carte:
+import { CAdminEditorPage } from 'coalesce-vue-vuetify/lib';
+
+const router = new Router({
+    // ...
+    routes: [
+        // ... other routes
+        {
+            path: '/admin/:type/edit/:id?',
+            name: 'coalesce-admin-item',
+            component: CAdminEditorPage,
+            props: true,
+        },
+    ]
+})
+

Props

`,3),h=n("p",null,"The PascalCase name of the type to be created/edited.",-1),C=n("p",null,[s("The primary key of the item being edited. If null or not provided, the page will be creating a new instance of the provided "),n("code",null,"type"),s(" instead.")],-1);function b(_,f){const a=o("RouterLink"),r=o("ExternalLinkIcon"),p=o("Prop");return i(),d("div",null,[m,t(" MARKER:summary "),n("p",null,[s("A page for a creating/editing single "),e(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[s("ViewModel")]),_:1}),s(" instance. Provides a "),e(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html"},{default:l(()=>[s("c-admin-editor")]),_:1}),s(" and a "),e(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html"},{default:l(()=>[s("c-admin-methods")]),_:1}),s(" for the instance. Designed to be routed to directly with "),n("a",u,[s("vue-router"),e(r)]),s(".")]),t(" MARKER:summary-end "),v,e(p,{def:"type: string",lang:"ts"}),h,e(p,{def:"id?: number | string",lang:"ts"}),C])}const E=c(y,[["render",b],["__file","c-admin-editor-page.html.vue"]]);export{E as default}; diff --git a/assets/c-admin-editor.html.34b7a2e3.js b/assets/c-admin-editor.html.34b7a2e3.js new file mode 100644 index 000000000..f983bd95c --- /dev/null +++ b/assets/c-admin-editor.html.34b7a2e3.js @@ -0,0 +1,2 @@ +import{_ as d,y as i,z as c,W as n,X as s,B as e,Q as a,$ as t,a5 as p,P as l}from"./framework.fe9a73df.js";const m={},u=s("h1",{id:"c-admin-editor",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-admin-editor","aria-hidden":"true"},"#"),e(" c-admin-editor")],-1),h=p(`

Examples

<c-admin-editor :model="person" />
+

Props

`,3);function _(f,v){const o=l("RouterLink"),r=l("Prop");return i(),c("div",null,[u,n(" MARKER:summary "),s("p",null,[e("An editor for a single "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:t(()=>[e("ViewModel")]),_:1}),e(" instance. Provides a "),a(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:t(()=>[e("c-input")]),_:1}),e(" for each property of the model.")]),n(" MARKER:summary-end "),s("p",null,[e("Does not automatically enable "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:t(()=>[e("auto-save")]),_:1}),e(" - if desired, this must be enabled by the implementor of this component.")]),h,a(r,{def:"model: ViewModel | ListViewModel",lang:"ts"}),s("p",null,[e("The "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:t(()=>[e("ViewModel")]),_:1}),e(" to render an editor for.")])])}const D=d(m,[["render",_],["__file","c-admin-editor.html.vue"]]);export{D as default}; diff --git a/assets/c-admin-editor.html.3d2a0299.js b/assets/c-admin-editor.html.3d2a0299.js new file mode 100644 index 000000000..1563d0b4d --- /dev/null +++ b/assets/c-admin-editor.html.3d2a0299.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-2a215065","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html","title":"c-admin-editor","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.md"}');export{e as data}; diff --git a/assets/c-admin-method.html.7f900f5d.js b/assets/c-admin-method.html.7f900f5d.js new file mode 100644 index 000000000..e633d0706 --- /dev/null +++ b/assets/c-admin-method.html.7f900f5d.js @@ -0,0 +1,2 @@ +import{_ as r,y as i,z as c,W as l,X as t,B as e,Q as a,$ as s,a5 as p,P as d}from"./framework.fe9a73df.js";const h={},m=t("h1",{id:"c-admin-method",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#c-admin-method","aria-hidden":"true"},"#"),e(" c-admin-method")],-1),u=p(`

Examples

<c-admin-method :model="person" for="setTitle" auto-reload-model />
+

Props

`,3),f=t("p",null,"A metadata specifier for the method. One of:",-1),_=t("ul",null,[t("li",null,[e("A string with the name of the method belonging to "),t("code",null,"model"),e(".")]),t("li",null,"A direct reference to a method's metadata object."),t("li",null,"A string in dot-notation that starts with a type name and ending with a method name.")],-1),v=t("code",null,"for",-1),y=t("p",null,[e("True if the "),t("code",null,"model"),e(" should have its "),t("code",null,"$load"),e(" invoked after a successful invocation of the method.")],-1);function D(g,w){const o=d("RouterLink"),n=d("Prop");return i(),c("div",null,[m,l(" MARKER:summary "),t("p",null,[e("Provides an interface for invoking a "),a(o,{to:"/modeling/model-components/methods.html"},{default:s(()=>[e("method")]),_:1}),e(" and rendering its result, designed to be use in an admin page.")]),l(" MARKER:summary-end "),t("p",null,[e("For each parameter of a method, a "),a(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:s(()=>[e("c-input")]),_:1}),e(" will be rendered to accept the input of that parameter. A button is provided to trigger an invocation of the method, progress and errors are rendered with a "),a(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html"},{default:s(()=>[e("c-loader-status")]),_:1}),e(", and results are rendered with "),a(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:s(()=>[e("c-display")]),_:1}),e(".")]),u,a(n,{def:"for: string | Method",lang:"ts"}),f,_,a(n,{def:"model: ViewModel | ListViewModel",lang:"ts"}),t("p",null,[e("An "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("ViewModel")]),_:1}),e(" or "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("ListViewModel")]),_:1}),e(" owning the method and "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" that was specified by the "),v,e(" prop.")]),a(n,{def:"autoReloadModel?: boolean = false",lang:"ts"}),y])}const k=r(h,[["render",D],["__file","c-admin-method.html.vue"]]);export{k as default}; diff --git a/assets/c-admin-method.html.add7fa03.js b/assets/c-admin-method.html.add7fa03.js new file mode 100644 index 000000000..c579a4b28 --- /dev/null +++ b/assets/c-admin-method.html.add7fa03.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-2e0f2971","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html","title":"c-admin-method","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-method.md"}');export{e as data}; diff --git a/assets/c-admin-methods.html.4198ca7c.js b/assets/c-admin-methods.html.4198ca7c.js new file mode 100644 index 000000000..95cd152d4 --- /dev/null +++ b/assets/c-admin-methods.html.4198ca7c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-065dbaae","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html","title":"c-admin-methods","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.md"}');export{e as data}; diff --git a/assets/c-admin-methods.html.98c2a74b.js b/assets/c-admin-methods.html.98c2a74b.js new file mode 100644 index 000000000..f573664a7 --- /dev/null +++ b/assets/c-admin-methods.html.98c2a74b.js @@ -0,0 +1,4 @@ +import{_ as d,y as p,z as i,W as c,X as a,B as e,Q as s,$ as n,a5 as m,P as l}from"./framework.fe9a73df.js";const u={},D=a("h1",{id:"c-admin-methods",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#c-admin-methods","aria-hidden":"true"},"#"),e(" c-admin-methods")],-1),h={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},y={href:"https://vuetifyjs.com/en/components/expansion-panels/",target:"_blank",rel:"noopener noreferrer"},v=m(`

Examples

<c-admin-methods :model="person" class="x" auto-reload-model />
+
<c-admin-methods :model="person" auto-reload-model />
+
<c-admin-methods :model="personList" auto-reload-model />
+

Props

`,5),_=a("p",null,[e("True if the "),a("code",null,"model"),e(" should have its "),a("code",null,"$load"),e(" invoked after a successful invocation of any method.")],-1);function f(C,E){const t=l("ExternalLinkIcon"),o=l("RouterLink"),r=l("Prop");return p(),i("div",null,[D,c(" MARKER:summary "),a("p",null,[e("Renders in a "),a("a",h,[e("Vuetify"),s(t)]),e(),a("a",y,[e("v-expansion-panels"),s(t)]),e(" a "),s(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html"},{default:n(()=>[e("c-admin-method")]),_:1}),e(" for each method on a "),s(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:n(()=>[e("ViewModel")]),_:1}),e(" or "),s(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:n(()=>[e("ListViewModel")]),_:1}),e(".")]),c(" MARKER:summary-end "),v,s(r,{def:"model: ViewModel | ListViewModel",lang:"ts"}),a("p",null,[e("An "),s(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:n(()=>[e("ViewModel")]),_:1}),e(" or "),s(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:n(()=>[e("ListViewModel")]),_:1}),e(" whose methods should each render as a "),s(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html"},{default:n(()=>[e("c-admin-method")]),_:1}),e(".")]),s(r,{def:"autoReloadModel?: boolean = false",lang:"ts"}),_])}const x=d(u,[["render",f],["__file","c-admin-methods.html.vue"]]);export{x as default}; diff --git a/assets/c-admin-table-page.html.3938dbb7.js b/assets/c-admin-table-page.html.3938dbb7.js new file mode 100644 index 000000000..a44fbfe93 --- /dev/null +++ b/assets/c-admin-table-page.html.3938dbb7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-72d994c4","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html","title":"c-admin-table-page","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.md"}');export{e as data}; diff --git a/assets/c-admin-table-page.html.9501ce38.js b/assets/c-admin-table-page.html.9501ce38.js new file mode 100644 index 000000000..928be91f6 --- /dev/null +++ b/assets/c-admin-table-page.html.9501ce38.js @@ -0,0 +1,20 @@ +import{_ as r,y as i,z as d,W as p,X as n,B as s,Q as a,$ as l,a5 as D,P as o}from"./framework.fe9a73df.js";const y={},m=n("h1",{id:"c-admin-table-page",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#c-admin-table-page","aria-hidden":"true"},"#"),s(" c-admin-table-page")],-1),u={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},v=D(`

Examples

// router.ts or main.ts
+
+// WITHOUT Vuetify A la carte:
+import { CAdminTablePage } from 'coalesce-vue-vuetify';
+// WITH Vuetify A-la-carte:
+import { CAdminTablePage } from 'coalesce-vue-vuetify/lib';
+
+const router = new Router({
+    // ...
+    routes: [
+        // ... other routes
+        {
+            path: '/admin/:type',
+            name: 'coalesce-admin-list',
+            component: CAdminTablePage,
+            props: true,
+        },
+    ]
+})
+

Props

`,3),h=n("p",null,"The PascalCase name of the type to be listed.",-1);function C(b,f){const e=o("RouterLink"),c=o("ExternalLinkIcon"),t=o("Prop");return i(),d("div",null,[m,p(" MARKER:summary "),n("p",null,[s("A full-featured page for interacting with a "),a(e,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[s("ListViewModel")]),_:1}),s(". Provides a "),a(e,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html"},{default:l(()=>[s("c-admin-table")]),_:1}),s(" and a "),a(e,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html"},{default:l(()=>[s("c-admin-methods")]),_:1}),s(" for the list. Designed to be routed to directly with "),n("a",u,[s("vue-router"),a(c)]),s(".")]),p(" MARKER:summary-end "),v,a(t,{def:"type: string",lang:"ts"}),h,a(t,{def:"list?: ListViewModel",lang:"ts"}),n("p",null,[s("An optional "),a(e,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[s("ListViewModel")]),_:1}),s(" that will be used if provided instead of the one the component will otherwise create automatically.")])])}const E=r(y,[["render",C],["__file","c-admin-table-page.html.vue"]]);export{E as default}; diff --git a/assets/c-admin-table-toolbar.html.20a09678.js b/assets/c-admin-table-toolbar.html.20a09678.js new file mode 100644 index 000000000..abeee2731 --- /dev/null +++ b/assets/c-admin-table-toolbar.html.20a09678.js @@ -0,0 +1,3 @@ +import{_ as p,y as i,z as d,W as r,X as a,B as e,Q as s,$ as l,a5 as u,P as n}from"./framework.fe9a73df.js";const D={},m=a("h1",{id:"c-admin-table-toolbar",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#c-admin-table-toolbar","aria-hidden":"true"},"#"),e(" c-admin-table-toolbar")],-1),h=u(`

Examples

<c-admin-table-toolbar :list="personList" />
+
<c-admin-table-toolbar :list="personList" color="pink" :editable.sync="isEditable" />
+

Props

`,4),y={href:"https://vuetifyjs.com/en/styles/colors/",target:"_blank",rel:"noopener noreferrer"},b=a("p",null,[e("If provided, adds a button to toggle the value of this prop. Should be bound with the "),a("code",null,".sync"),e(" modifier.")],-1);function v(f,_){const o=n("RouterLink"),t=n("Prop"),c=n("ExternalLinkIcon");return i(),d("div",null,[m,r(" MARKER:summary "),a("p",null,[e("A full-featured toolbar for a "),s(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[e("ListViewModel")]),_:1}),e(' designed to be used on an admin page, including "Create" and "Reload" buttons, a '),s(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html"},{default:l(()=>[e("c-list-range-display")]),_:1}),e(", a "),s(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html"},{default:l(()=>[e("c-list-page")]),_:1}),e(", a search field, "),s(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html"},{default:l(()=>[e("c-list-filters")]),_:1}),e(", and a "),s(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:l(()=>[e("c-list-page-size")]),_:1}),e(".")]),r(" MARKER:summary-end "),h,s(t,{def:"list: ListViewModel",lang:"ts"}),a("p",null,[e("The "),s(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[e("ListViewModel")]),_:1}),e(" to render the toolbar for.")]),s(t,{def:"color: string = 'primary'",lang:"ts"}),a("p",null,[e("The "),a("a",y,[e("color"),s(c)]),e(" of the toolbar.")]),s(t,{def:"editable?: boolean",lang:"ts"}),b])}const C=p(D,[["render",v],["__file","c-admin-table-toolbar.html.vue"]]);export{C as default}; diff --git a/assets/c-admin-table-toolbar.html.c459a0a0.js b/assets/c-admin-table-toolbar.html.c459a0a0.js new file mode 100644 index 000000000..32427198a --- /dev/null +++ b/assets/c-admin-table-toolbar.html.c459a0a0.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6bac3238","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html","title":"c-admin-table-toolbar","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.md"}');export{e as data}; diff --git a/assets/c-admin-table.html.698a17a1.js b/assets/c-admin-table.html.698a17a1.js new file mode 100644 index 000000000..d9be62c33 --- /dev/null +++ b/assets/c-admin-table.html.698a17a1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-335a61d2","path":"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html","title":"c-admin-table","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-admin-table.md"}');export{e as data}; diff --git a/assets/c-admin-table.html.b1ffc448.js b/assets/c-admin-table.html.b1ffc448.js new file mode 100644 index 000000000..482cd07ef --- /dev/null +++ b/assets/c-admin-table.html.b1ffc448.js @@ -0,0 +1,2 @@ +import{_ as c,y as d,z as r,W as n,X as o,B as e,Q as t,$ as s,a5 as u,P as i}from"./framework.fe9a73df.js";const p={},m=o("h1",{id:"c-admin-table",tabindex:"-1"},[o("a",{class:"header-anchor",href:"#c-admin-table","aria-hidden":"true"},"#"),e(" c-admin-table")],-1),h=u(`

Examples

<c-admin-table :list="personList" />
+

Props

`,3),f=o("code",null,"[10, 25, 100]",-1);function v(b,_){const a=i("RouterLink"),l=i("Prop");return d(),r("div",null,[m,n(" MARKER:summary "),o("p",null,[e("An full-featured table for a "),t(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("ListViewModel")]),_:1}),e(", including a "),t(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html"},{default:s(()=>[e("c-admin-table-toolbar")]),_:1}),e(", "),t(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-table.html"},{default:s(()=>[e("c-table")]),_:1}),e(", and "),t(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html"},{default:s(()=>[e("c-list-pagination")]),_:1}),e(".")]),n(" MARKER:summary-end "),o("p",null,[e("The table can be in read mode (default), or toggled into edit mode with the button provided by the "),t(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html"},{default:s(()=>[e("c-admin-table-toolbar")]),_:1}),e(". When placed into edit mode, "),t(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("auto-save")]),_:1}),e(" is enabled.")]),h,t(l,{def:"list: ListViewModel",lang:"ts"}),o("p",null,[e("The "),t(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("ListViewModel")]),_:1}),e(" to render a table for.")]),t(l,{def:"pageSizes?: number[]",lang:"ts"}),o("p",null,[e("An optional list of available page sizes to offer through the "),t(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html"},{default:s(()=>[e("c-list-pagination")]),_:1}),e("'s "),t(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:s(()=>[e("c-list-page-size")]),_:1}),e(" component. Defaults to "),f,e(".")]),t(l,{def:"queryBind?: boolean",lang:"ts"}),o("p",null,[e("If true, the "),t(a,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Data Source Standard Parameters")]),_:1}),e(" of the provided "),t(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("ListViewModel")]),_:1}),e(` will be read from and written to the window's query string. The "Editable" state of the table will also be bound to the query string.`)])])}const g=c(p,[["render",v],["__file","c-admin-table.html.vue"]]);export{g as default}; diff --git a/assets/c-datetime-picker.html.061c28e5.js b/assets/c-datetime-picker.html.061c28e5.js new file mode 100644 index 000000000..8944dd526 --- /dev/null +++ b/assets/c-datetime-picker.html.061c28e5.js @@ -0,0 +1,11 @@ +import{_ as p,y as u,z as h,W as d,X as e,B as t,Q as s,$ as a,a5 as c,P as o}from"./framework.fe9a73df.js";const m={},D=e("h1",{id:"c-datetime-picker",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#c-datetime-picker","aria-hidden":"true"},"#"),t(" c-datetime-picker")],-1),y=e("code",null,"v-model",-1),f={class:"table-of-contents"},_=c(`

Examples

<c-datetime-picker :model="person" for="birthDate" />
+
+<c-datetime-picker v-model="standaloneDate" />
+
+<c-datetime-picker 
+    v-model="standaloneTime" 
+    date-kind="time"
+    date-format="h:mm a"
+/>
+

Props

`,3),b=e("p",null,"A metadata specifier for the value being bound. One of:",-1),v=e("ul",null,[e("li",null,[t("A string with the name of the value belonging to "),e("code",null,"model"),t(".")]),e("li",null,"A direct reference to a metadata object."),e("li",null,"A string in dot-notation that starts with a type name.")],-1),g=e("p",null,[t("An object owning the value that was specified by the "),e("code",null,"for"),t(" prop. If provided, the input will be bound to the corresponding property on the "),e("code",null,"model"),t(" object.")],-1),k=e("p",null,[t("If binding the component with "),e("code",null,"v-model"),t(", accepts the "),e("code",null,"value"),t(" part of "),e("code",null,"v-model"),t(".")],-1),C=e("p",null,[t("Whether the date is only a date, only a time, or contains significant date "),e("code",null,"and"),t(" time information.")],-1),w=e("code",null,"for",-1),E=c("

The format of the date that will be rendered in the component's text field, and the format that will be attempted first when parsing user input in the text field.

Defaults to:

  • M/d/yyyy h:mm a if dateKind == 'datetime',
  • M/d/yyyy if dateKind == 'date', or
  • h:mm a if dateKind == 'time'.
",3),x={class:"custom-container warning"},q=e("p",{class:"custom-container-title"},"WARNING",-1),F=e("p",null,[t("When parsing a user's text input into the text field, c-datetime-picker will first attempt to parse it with the format specified by "),e("code",null,"dateFormat"),t(", or the default as described above if not explicitly specified.")],-1),T={href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date",target:"_blank",rel:"noopener noreferrer"},V=e("code",null,"dateKind",-1),A=e("code",null,"datetime",-1),R=e("code",null,"date",-1),I=e("p",null,"True if a native HTML5 input should be used instead of a popup menu with Vuetify date/time pickers inside of it.",-1),K=e("p",null,"True if the calendar and clock should be shown side by side in the picker menu, rather than in separate tabs.",-1),N=e("p",null,"True if the component should be read-only.",-1),P=e("p",null,"True if the component should be disabled.",-1);function M(B,L){const l=o("RouterLink"),i=o("router-link"),n=o("Prop"),r=o("ExternalLinkIcon");return u(),h("div",null,[D,d(" MARKER:summary "),e("p",null,[t("A general, all-purpose date/time input component that can be used either with "),s(l,{to:"/stacks/vue/layers/models.html"},{default:a(()=>[t("models")]),_:1}),t(" and "),s(l,{to:"/stacks/vue/layers/metadata.html"},{default:a(()=>[t("metadata")]),_:1}),t(" or as a standalone component using only "),y,t(".")]),d(" MARKER:summary-end "),e("nav",f,[e("ul",null,[e("li",null,[s(i,{to:"#examples"},{default:a(()=>[t("Examples")]),_:1})]),e("li",null,[s(i,{to:"#props"},{default:a(()=>[t("Props")]),_:1})])])]),_,s(n,{def:"for?: string | DateProperty | DateValue",lang:"ts"}),b,v,s(n,{def:"model?: Model | DataSource",lang:"ts"}),g,s(n,{def:`value?: Date // Vue 2 +modelValue?: Date // Vue 3`,lang:"ts"}),k,s(n,{def:"dateKind?: 'date' | 'time' | 'datetime' = 'datetime'",lang:"ts"}),C,e("p",null,[t("If the component was bound with metadata using the "),w,t(" prop, this will default to the kind specified by "),s(l,{to:"/modeling/model-components/attributes/date-type.html"},{default:a(()=>[t("[DateType]")]),_:1}),t(".")]),s(n,{def:"dateFormat?: string",lang:"ts"}),E,e("div",x,[q,F,e("p",null,[t("If this fails, the date will be parsed with the "),e("a",T,[t("Date constructor"),s(r)]),t(", but only if the "),V,t(" is "),A,t(" or "),R,t(". This works fairly well on all modern browsers, but can still occasionally have issues. c-datetime-picker tries its best to filter out bad parses from the Date constructor, like dates with a year earlier than 1000.")])]),s(n,{def:"native?: boolean",lang:"ts"}),I,s(n,{def:"sideBySide?: boolean",lang:"ts"}),K,s(n,{def:"readonly?: boolean",lang:"ts"}),N,s(n,{def:"disabled?: boolean",lang:"ts"}),P])}const W=p(m,[["render",M],["__file","c-datetime-picker.html.vue"]]);export{W as default}; diff --git a/assets/c-datetime-picker.html.34960e92.js b/assets/c-datetime-picker.html.34960e92.js new file mode 100644 index 000000000..69bec88f1 --- /dev/null +++ b/assets/c-datetime-picker.html.34960e92.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0c72dc02","path":"/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html","title":"c-datetime-picker","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.md"}');export{e as data}; diff --git a/assets/c-display.html.61470104.js b/assets/c-display.html.61470104.js new file mode 100644 index 000000000..7f36d641e --- /dev/null +++ b/assets/c-display.html.61470104.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-76b68ec0","path":"/stacks/vue/coalesce-vue-vuetify/components/c-display.html","title":"c-display","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Slots","slug":"slots","link":"#slots","children":[]},{"level":2,"title":"[DataTypeAttribute]","slug":"datatypeattribute","link":"#datatypeattribute","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-display.md"}');export{e as data}; diff --git a/assets/c-display.html.dabbb75d.js b/assets/c-display.html.dabbb75d.js new file mode 100644 index 000000000..9bc4d029e --- /dev/null +++ b/assets/c-display.html.dabbb75d.js @@ -0,0 +1,10 @@ +import{_ as d,y as u,z as D,W as c,X as s,B as e,Q as a,$ as o,a5 as r,P as p}from"./framework.fe9a73df.js";const y={},h=s("h1",{id:"c-display",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-display","aria-hidden":"true"},"#"),e(" c-display")],-1),m={class:"table-of-contents"},f=r(`

Examples

Typical usage, providing an object and a property on that object:

<c-display :model="person" for="gender" />
+
`,3),v={href:"https://date-fns.org/v2.29.3/docs/format",target:"_blank",rel:"noopener noreferrer"},g=r(`
<c-display :model="person" for="birthDate" format="M/d/yyyy" />
+
`,1),b=r(`
<c-display 
+    :value="person.setFirstName.result" 
+    :for="person.$metadata.methods.setFirstName.return" 
+    element="div"
+/>
+

Displaying a standalone date value without a model or other source of metadata:

<c-display :value="dateProp" format="M/d/yyyy" />
+

Props

`,4),C=s("p",null,[e("A metadata specifier for the value being bound. Either a direct reference to the metadata object, or a string with the name of the value belonging to "),s("code",null,"model"),e(", or a string in dot-notation that starts with a type name.")],-1),_=s("p",null,[e("An object owning the value that was specified by the "),s("code",null,"for"),e(" prop.")],-1),E=s("p",null,[e("Shorthand for "),s("code",null,':options="{ format: format }"'),e(", allowing for specification of the format to be used when displaying dates.")],-1),k=s("code",null,"format",-1),F=r('

Can be provided the value to be displayed in conjunction with the for prop, as an alternative to the model prop.

This is an uncommon scenario - it is generally easier to use the for/model props together.

Slots

default - Used to display fallback content if the value being displayed is either null or "" (empty string).

[DataTypeAttribute]

For properties and other values annotated with [DataTypeAttribute], the following special handling occurs based on the data type:

  • DataType.MultilineText: Renders with white-space: pre-wrap.
  • DataType.Password: Renders with a show/hide toggle (hidden by default), showing a fixed number of dot characters when hidden.
  • DataType.Url: Renders as a clickable link.
  • DataType.EmailAddress: Renders as a clickable mailto link.
  • DataType.PhoneNumber: Renders as a clickable tel link.
  • DataType.ImageUrl: Renders as an img element.
  • "Color": Renders a colored dot next to the value, interpreting the field value as a 7-character HTML hex color code..
',7);function x(q,w){const l=p("RouterLink"),t=p("router-link"),i=p("ExternalLinkIcon"),n=p("Prop");return u(),D("div",null,[h,c(" MARKER:summary "),s("p",null,[e("A general-purpose component for displaying any "),a(l,{to:"/stacks/vue/layers/metadata.html#value"},{default:o(()=>[e("Value")]),_:1}),e(" by rendering the value to a string with the "),a(l,{to:"/stacks/vue/layers/models.html#VueModelDisplayFunctions"},{default:o(()=>[e("display functions from the Models Layer")]),_:1}),e(". For plain string and number "),a(l,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("values")]),_:1}),e(", usage of this component is largely superfluous. For all other value types including dates, booleans, enums, objects, and collections, it is very handy.")]),c(" MARKER:summary-end "),s("nav",m,[s("ul",null,[s("li",null,[a(t,{to:"#examples"},{default:o(()=>[e("Examples")]),_:1})]),s("li",null,[a(t,{to:"#props"},{default:o(()=>[e("Props")]),_:1})]),s("li",null,[a(t,{to:"#slots"},{default:o(()=>[e("Slots")]),_:1})]),s("li",null,[a(t,{to:"#datatypeattribute"},{default:o(()=>[e("[DataTypeAttribute]")]),_:1})])])]),f,s("p",null,[e("Customizing date formatting ("),s("a",v,[e("view format patterns"),a(i)]),e("):")]),g,s("p",null,[e("A contrived example of using c-display to render the result of an "),a(l,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:o(()=>[e("API Caller")]),_:1}),e(":")]),b,a(n,{def:"for: string | Property | Value",lang:"ts"}),C,a(n,{def:"model?: Model | DataSource",lang:"ts"}),_,a(n,{def:'format: DisplayOptions["format"]',lang:"ts"}),E,s("p",null,[e("See "),a(l,{to:"/stacks/vue/layers/models.html#displayoptions"},{default:o(()=>[e("DisplayOptions")]),_:1}),e(" for details on the options available for "),k,e(".")]),a(n,{def:"options: DisplayOptions",lang:"ts"}),s("p",null,[e("Specify options for formatting some kinds of values, including dates. See "),a(l,{to:"/stacks/vue/layers/models.html#displayoptions"},{default:o(()=>[e("DisplayOptions")]),_:1}),e(" for details.")]),a(n,{def:`value: any // Vue 2 +modelValue: any // Vue 3`,lang:"ts"}),F])}const R=d(y,[["render",x],["__file","c-display.html.vue"]]);export{R as default}; diff --git a/assets/c-input.html.d42429ba.js b/assets/c-input.html.d42429ba.js new file mode 100644 index 000000000..9aa339f7f --- /dev/null +++ b/assets/c-input.html.d42429ba.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-82d04650","path":"/stacks/vue/coalesce-vue-vuetify/components/c-input.html","title":"c-input","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Slots","slug":"slots","link":"#slots","children":[]}],"git":{"updatedTime":1677795733000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-input.md"}');export{e as data}; diff --git a/assets/c-input.html.e13a8a1c.js b/assets/c-input.html.e13a8a1c.js new file mode 100644 index 000000000..0fa76d6cc --- /dev/null +++ b/assets/c-input.html.e13a8a1c.js @@ -0,0 +1,12 @@ +import{_ as d,y as u,z as D,W as a,X as s,B as e,Q as o,$ as n,a5 as p,P as r}from"./framework.fe9a73df.js";const y={},h=s("h1",{id:"c-input",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-input","aria-hidden":"true"},"#"),e(" c-input")],-1),m={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},f={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},v=s("p",null,"A summary of the components delegated to, by type:",-1),_={href:"https://vuetifyjs.com/en/components/textarea/",target:"_blank",rel:"noopener noreferrer"},b=s("code",null,"textarea",-1),g=s("code",null,"c-input",-1),C=s("code",null,"[DataType(DataType.MultilineText)]",-1),E={href:"https://vuetifyjs.com/en/components/text-fields/",target:"_blank",rel:"noopener noreferrer"},k=s("code",null,"[DataTypeAttribute]",-1),x=s("code",null,"DataType.EmailAddress",-1),w=s("code",null,"DataType.PhoneNumber",-1),F=s("code",null,"DataType.Password",-1),q=s("code",null,'"Color"',-1),j={href:"https://vuetifyjs.com/en/components/text-fields/",target:"_blank",rel:"noopener noreferrer"},A={href:"https://vuetifyjs.com/en/components/selection-controls/",target:"_blank",rel:"noopener noreferrer"},V={href:"https://vuetifyjs.com/en/components/selection-controls/",target:"_blank",rel:"noopener noreferrer"},N=s("code",null,"checkbox",-1),P=s("code",null,"c-input",-1),T={href:"https://vuetifyjs.com/en/components/selects/",target:"_blank",rel:"noopener noreferrer"},M={href:"https://vuetifyjs.com/en/components/file-inputs/",target:"_blank",rel:"noopener noreferrer"},R={href:"https://vuejs.org/guide/components/slots.html",target:"_blank",rel:"noopener noreferrer"},B={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},I=s("code",null,"rules",-1),S={class:"table-of-contents"},L=p(`

Examples

Typical usage, providing an object and a property on that object:

<c-input :model="person" for="firstName" />
+
`,3),K={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},z=p(`
<c-input :model="comment" for="content" textarea solo />
+
`,1),O=p(`
<c-input 
+    :model="person.setFirstName" 
+    for="newName" />
+

Or, using a more verbose syntax:

<c-input 
+    :model="person.setFirstName.args" 
+    for="Person.methods.setFirstName.newName" />
+
`,3),W=p(`
<c-input :model="personList.$dataSource" for="startsWith" />
+

Usage with v-model (this scenario is atypical - the model/for pair of props are used in almost all scenarios):

<c-input v-model="person.firstName" for="Person.firstName" />
+

Props

`,4),U=s("p",null,"A metadata specifier for the value being bound. One of:",-1),$=s("ul",null,[s("li",null,[e("A string with the name of the value belonging to "),s("code",null,"model"),e(".")]),s("li",null,"A direct reference to a metadata object."),s("li",null,"A string in dot-notation that starts with a type name.")],-1),G=s("p",null,[e("An object owning the value that was specified by the "),s("code",null,"for"),e(" prop. If provided, the input will be bound to the corresponding property on the "),s("code",null,"model"),e(" object.")],-1),Q=s("p",null,[e("If binding the component with "),s("code",null,"v-model"),e(", accepts the "),s("code",null,"value"),e(" part of "),s("code",null,"v-model"),e(".")],-1),X=s("h2",{id:"slots",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#slots","aria-hidden":"true"},"#"),e(" Slots")],-1),H=s("p",null,[s("code",null,"default"),e(" - Used to display fallback content if c-input does not support the type of the value being bound. Generally this does not need to be used, as you should avoid creating c-input components for unsupported types in the first place.")],-1);function J(Y,Z){const t=r("RouterLink"),l=r("ExternalLinkIcon"),c=r("router-link"),i=r("Prop");return u(),D("div",null,[h,a(" MARKER:summary "),s("p",null,[e("A general-purpose input component for most "),o(t,{to:"/stacks/vue/layers/metadata.html"},{default:n(()=>[e("Values")]),_:1}),e(". c-input does not have much functionality of its own - instead, it delegates to the right kind of component based on the type of value to which it is bound. This includes both other "),o(t,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:n(()=>[e("Coalesce Vuetify Components")]),_:1}),e(" as well as direct usages of some "),s("a",m,[e("Vuetify"),o(l)]),e(" components.")]),a(" MARKER:summary-end "),s("p",null,[e("All attributes are passed through to the delegated-to component, allowing for full customization of the underlying "),s("a",f,[e("Vuetify"),o(l)]),e(" component.")]),v,s("ul",null,[s("li",null,[e("string: "),s("ul",null,[s("li",null,[s("a",_,[e("v-textarea"),o(l)]),e(" if flag attribute "),b,e(" is provided to "),g,e(" or if "),C,e(" is present in C#.")]),s("li",null,[e("Otherwise, "),s("a",E,[e("v-text-field"),o(l)]),e(". Additionally, "),k,e(" values of "),x,e(", "),w,e(", "),F,e(", or "),q,e(" on the field will apply appropriate adjustments to the field.")])])]),s("li",null,[e("number: "),s("a",j,[e("v-text-field"),o(l)]),e(".")]),s("li",null,[e("boolean: "),s("a",A,[e("v-switch"),o(l)]),e(", or "),s("a",V,[e("v-checkbox"),o(l)]),e(" if flag attribute "),N,e(" is provided to "),P,e(".")]),s("li",null,[e("enum: "),s("a",T,[e("v-select"),o(l)])]),s("li",null,[e("file: "),s("a",M,[e("v-file-input"),o(l)])]),s("li",null,[e("date: "),o(t,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html"},{default:n(()=>[e("c-datetime-picker")]),_:1})]),s("li",null,[e("model: "),o(t,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select.html"},{default:n(()=>[e("c-select")]),_:1})]),s("li",null,[o(t,{to:"/modeling/model-components/attributes/many-to-many.html"},{default:n(()=>[e("[ManyToMany]")]),_:1}),e(" collection: "),o(t,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html"},{default:n(()=>[e("c-select-many-to-many")]),_:1})]),s("li",null,[e("Non-object collection: "),o(t,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html"},{default:n(()=>[e("c-select-values")]),_:1})])]),s("p",null,[e("Any other unsupported type will simply be displayed with "),o(t,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:n(()=>[e("c-display")]),_:1}),e(", unless a "),s("a",R,[e("default slot"),o(l)]),e(" is provided - in that case, the default slot will be rendered instead.")]),s("p",null,[e("When bound to a "),o(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:n(()=>[e("ViewModel")]),_:1}),e(", the "),o(t,{to:"/stacks/vue/layers/viewmodels.html#rules-validation"},{default:n(()=>[e("validation rules")]),_:1}),e(" for the bound property will be obtained from the "),o(t,{to:"/stacks/vue/layers/viewmodels.html#rules-validation"},{default:n(()=>[e("ViewModel")]),_:1}),e(" and passed to "),s("a",B,[e("Vuetify"),o(l)]),e("'s "),I,e(" prop.")]),s("nav",S,[s("ul",null,[s("li",null,[o(c,{to:"#examples"},{default:n(()=>[e("Examples")]),_:1})]),s("li",null,[o(c,{to:"#props"},{default:n(()=>[e("Props")]),_:1})]),s("li",null,[o(c,{to:"#slots"},{default:n(()=>[e("Slots")]),_:1})])])]),L,s("p",null,[e("Customizing the "),s("a",K,[e("Vuetify"),o(l)]),e(" component used:")]),z,s("p",null,[e("Binding to "),o(t,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:n(()=>[e("API Caller")]),_:1}),e(" args objects:")]),O,s("p",null,[e("Binding to "),o(t,{to:"/modeling/model-components/data-sources.html#custom-parameters"},{default:n(()=>[e("Data Source Parameters")]),_:1}),e(":")]),W,a(" MARKER:c-for-model-props "),o(i,{def:"for?: string | Property | Value",lang:"ts"}),U,$,o(i,{def:"model?: Model | DataSource",lang:"ts"}),G,a(" MARKER:c-for-model-props-end "),o(i,{def:`value?: any // Vue 2 +modelValue?: any // Vue 3`,lang:"ts"}),Q,X,H])}const se=d(y,[["render",J],["__file","c-input.html.vue"]]);export{se as default}; diff --git a/assets/c-list-filters.html.67503626.js b/assets/c-list-filters.html.67503626.js new file mode 100644 index 000000000..f6c5264f8 --- /dev/null +++ b/assets/c-list-filters.html.67503626.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-417cf638","path":"/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html","title":"c-list-filters","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-list-filters.md"}');export{e as data}; diff --git a/assets/c-list-filters.html.91c1f54c.js b/assets/c-list-filters.html.91c1f54c.js new file mode 100644 index 000000000..d923ccb4b --- /dev/null +++ b/assets/c-list-filters.html.91c1f54c.js @@ -0,0 +1,2 @@ +import{_ as i,y as c,z as d,W as o,X as s,B as e,Q as a,$ as l,a5 as p,P as n}from"./framework.fe9a73df.js";const m={},h=s("h1",{id:"c-list-filters",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-list-filters","aria-hidden":"true"},"#"),e(" c-list-filters")],-1),u=s("code",null,"filters",-1),f=p(`

Example Usage

<c-list-filters :list="list" />
+

Props

`,3);function _(D,y){const t=n("RouterLink"),r=n("Prop");return c(),d("div",null,[h,o(" MARKER:summary "),s("p",null,[e("A component that provides an interface for modifying the "),u,e(" prop of a "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[e("ListViewModel")]),_:1}),e("'s "),a(t,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:l(()=>[e("parameters")]),_:1}),e(".")]),o(" MARKER:summary-end "),f,a(r,{def:"list: ListViewModel",lang:"ts"}),s("p",null,[e("The "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[e("ListViewModel")]),_:1}),e(" whose filters will be editable.")])])}const x=i(m,[["render",_],["__file","c-list-filters.html.vue"]]);export{x as default}; diff --git a/assets/c-list-page-size.html.24864951.js b/assets/c-list-page-size.html.24864951.js new file mode 100644 index 000000000..c1a571e0e --- /dev/null +++ b/assets/c-list-page-size.html.24864951.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-546d4960","path":"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html","title":"c-list-page-size","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.md"}');export{e as data}; diff --git a/assets/c-list-page-size.html.59698aa9.js b/assets/c-list-page-size.html.59698aa9.js new file mode 100644 index 000000000..155174fdc --- /dev/null +++ b/assets/c-list-page-size.html.59698aa9.js @@ -0,0 +1,2 @@ +import{_ as r,y as c,z as p,W as n,X as s,B as e,Q as a,$ as o,a5 as d,P as i}from"./framework.fe9a73df.js";const u={},h=s("h1",{id:"c-list-page-size",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-list-page-size","aria-hidden":"true"},"#"),e(" c-list-page-size")],-1),m=s("code",null,"pageSize",-1),_=d(`

Example Usage

<c-list-page-size :list="list" />
+

Props

`,3),f=s("code",null,"[10, 25, 100]",-1);function g(v,D){const t=i("RouterLink"),l=i("Prop");return c(),p("div",null,[h,n(" MARKER:summary "),s("p",null,[e("A component that provides an dropdown for modifying the "),m,e(),a(t,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:o(()=>[e("parameter")]),_:1}),e(" prop of a "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")]),n(" MARKER:summary-end "),_,a(l,{def:"list: ListViewModel",lang:"ts"}),s("p",null,[e("The "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(" whose pagination will be editable.")]),a(l,{def:"pageSizes?: number[]",lang:"ts"}),s("p",null,[e("An optional list of available page sizes to offer through "),a(t,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:o(()=>[e("c-list-page-size")]),_:1}),e(". Defaults to "),f,e(".")])])}const x=r(u,[["render",g],["__file","c-list-page-size.html.vue"]]);export{x as default}; diff --git a/assets/c-list-page.html.4b1df619.js b/assets/c-list-page.html.4b1df619.js new file mode 100644 index 000000000..6d58d56aa --- /dev/null +++ b/assets/c-list-page.html.4b1df619.js @@ -0,0 +1,2 @@ +import{_ as c,y as i,z as p,W as n,X as a,B as e,Q as s,$ as o,a5 as d,P as l}from"./framework.fe9a73df.js";const h={},m=a("h1",{id:"c-list-page",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#c-list-page","aria-hidden":"true"},"#"),e(" c-list-page")],-1),u=a("code",null,"page",-1),_=d(`

Example Usage

<c-list-page :list="list" />
+

Props

`,3);function g(f,D){const t=l("RouterLink"),r=l("Prop");return i(),p("div",null,[m,n(" MARKER:summary "),a("p",null,[e("A component that provides previous/next buttons and a text field for modifying the "),u,e(),s(t,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:o(()=>[e("parameter")]),_:1}),e(" prop of a "),s(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")]),n(" MARKER:summary-end "),_,s(r,{def:"list: ListViewModel",lang:"ts"}),a("p",null,[e("The "),s(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(" whose current page will be changeable with the component.")])])}const y=c(h,[["render",g],["__file","c-list-page.html.vue"]]);export{y as default}; diff --git a/assets/c-list-page.html.ea7168b8.js b/assets/c-list-page.html.ea7168b8.js new file mode 100644 index 000000000..8ee51c85e --- /dev/null +++ b/assets/c-list-page.html.ea7168b8.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-23f5be44","path":"/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html","title":"c-list-page","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-list-page.md"}');export{e as data}; diff --git a/assets/c-list-pagination.html.8d277ca9.js b/assets/c-list-pagination.html.8d277ca9.js new file mode 100644 index 000000000..41eda23bd --- /dev/null +++ b/assets/c-list-pagination.html.8d277ca9.js @@ -0,0 +1,2 @@ +import{_ as c,y as r,z as p,W as n,X as o,B as e,Q as a,$ as t,a5 as d,P as i}from"./framework.fe9a73df.js";const u={},m=o("h1",{id:"c-list-pagination",tabindex:"-1"},[o("a",{class:"header-anchor",href:"#c-list-pagination","aria-hidden":"true"},"#"),e(" c-list-pagination")],-1),h=d(`

Example Usage

<c-list-pagination :list="list" />
+

Props

`,3),f=o("code",null,"[10, 25, 100]",-1);function g(v,_){const s=i("RouterLink"),l=i("Prop");return r(),p("div",null,[m,n(" MARKER:summary "),o("p",null,[e("A component that provides an interface for modifying the pagination "),a(s,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:t(()=>[e("parameters")]),_:1}),e(" of a "),a(s,{to:"/stacks/vue/layers/viewmodels.html"},{default:t(()=>[e("ListViewModel")]),_:1}),e(".")]),o("p",null,[e("This is a composite of "),a(s,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:t(()=>[e("c-list-page-size")]),_:1}),e(", "),a(s,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html"},{default:t(()=>[e("c-list-range-display")]),_:1}),e(", and "),a(s,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html"},{default:t(()=>[e("c-list-page")]),_:1}),e(", arranged horizontally. It is designed to be used above or below a table (e.g. "),a(s,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-table.html"},{default:t(()=>[e("c-table")]),_:1}),e(").")]),n(" MARKER:summary-end "),h,a(l,{def:"list: ListViewModel",lang:"ts"}),o("p",null,[e("The "),a(s,{to:"/stacks/vue/layers/viewmodels.html"},{default:t(()=>[e("ListViewModel")]),_:1}),e(" whose pagination will be editable.")]),a(l,{def:"pageSizes?: number[]",lang:"ts"}),o("p",null,[e("An optional list of available page sizes to offer through "),a(s,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:t(()=>[e("c-list-page-size")]),_:1}),e(". Defaults to "),f,e(".")])])}const D=c(u,[["render",g],["__file","c-list-pagination.html.vue"]]);export{D as default}; diff --git a/assets/c-list-pagination.html.d2215a15.js b/assets/c-list-pagination.html.d2215a15.js new file mode 100644 index 000000000..098f3c9d7 --- /dev/null +++ b/assets/c-list-pagination.html.d2215a15.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-263706ce","path":"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html","title":"c-list-pagination","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.md"}');export{e as data}; diff --git a/assets/c-list-range-display.html.78677805.js b/assets/c-list-range-display.html.78677805.js new file mode 100644 index 000000000..0b781ad1b --- /dev/null +++ b/assets/c-list-range-display.html.78677805.js @@ -0,0 +1,2 @@ +import{_ as r,y as c,z as d,W as n,X as s,B as e,Q as a,$ as o,a5 as p,P as l}from"./framework.fe9a73df.js";const u={},h=s("h1",{id:"c-list-range-display",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-list-range-display","aria-hidden":"true"},"#"),e(" c-list-range-display")],-1),m=s("code",null,"$items",-1),_=s("code",null," - of ",-1),y=s("code",null,"$load",-1),f=s("code",null,"$params",-1),D=p(`

Examples

<c-list-range-display :list="list" />
+

Props

`,3);function v(g,x){const t=l("RouterLink"),i=l("Prop");return c(),d("div",null,[h,n(" MARKER:summary "),s("p",null,[e("Displays pagination information about the current "),m,e(" of a "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(" in the format "),_,e(".")]),n(" MARKER:summary-end "),s("p",null,[e("Uses the pagination information returned from the last successful "),y,e(" call, not the current "),f,e(" of the "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")]),D,a(i,{def:"list: ListViewModel",lang:"ts"}),s("p",null,[e("The "),a(t,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(" to display pagination information for.")])])}const E=r(u,[["render",v],["__file","c-list-range-display.html.vue"]]);export{E as default}; diff --git a/assets/c-list-range-display.html.8f5b0583.js b/assets/c-list-range-display.html.8f5b0583.js new file mode 100644 index 000000000..cc84c8d80 --- /dev/null +++ b/assets/c-list-range-display.html.8f5b0583.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4822168d","path":"/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html","title":"c-list-range-display","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.md"}');export{e as data}; diff --git a/assets/c-loader-status.html.00e079dd.js b/assets/c-loader-status.html.00e079dd.js new file mode 100644 index 000000000..b12016f94 --- /dev/null +++ b/assets/c-loader-status.html.00e079dd.js @@ -0,0 +1,29 @@ +import{_ as d,y as D,z as y,W as c,X as e,B as s,Q as n,$ as a,a5 as i,P as t}from"./framework.fe9a73df.js";const u={},h=e("h1",{id:"c-loader-status",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#c-loader-status","aria-hidden":"true"},"#"),s(" c-loader-status")],-1),m={class:"custom-container tip"},v=e("p",{class:"custom-container-title"},"TIP",-1),f={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},C={href:"https://vuetifyjs.com/en/components/progress-linear",target:"_blank",rel:"noopener noreferrer"},b={href:"https://vuetifyjs.com/en/components/alerts/",target:"_blank",rel:"noopener noreferrer"},g={href:"https://vuetifyjs.com/en/styles/transitions/",target:"_blank",rel:"noopener noreferrer"},_={class:"custom-container tip"},E=e("p",{class:"custom-container-title"},"Note",-1),w=e("code",null,"c-caller-status",-1),F=e("code",null,"c-loader-status",-1),k={class:"table-of-contents"},x=i(`

Examples

Wrap contents of a details/edit page:

<h1>Person Details</h1>
+<c-loader-status
+    :loaders="{ 
+        'no-initial-content no-error-content': [person.$load],
+        '': [person.$save, person.$delete],
+    }"
+    #default
+>
+    First Name: {{ person.firstName }}
+    Last Name: {{ person.lastName }}
+    Employer: {{ person.company.name }}
+</c-loader-status>
+

Use c-loader-status to render a progress bar and any error messages, but don't use it to control content:

<c-loader-status :loaders="{'': [list.$load]}" />
+

Wrap a save/submit button:

<c-loader-status
+    :loaders="{ 'no-loading-content': [person.$save] }"
+>
+    <button> Save </button>
+</c-loader-status>
+

Hides the table before the first load has completed, or if loading the list encountered an error. Don't show the progress bar after we've already loaded the list for the first time (useful for loads that occur without user interaction, e.g. setInterval):

<c-loader-status
+    :loaders="{
+        'no-secondary-progress no-initial-content no-error-content': [list.$load]
+    }"
+    #default
+>
+    <table>
+        <tr v-for="item in list.$items"> ... </tr>
+    </table>
+</c-loader-status>
+

Props

`,10),P=i('

The available flags are as follows. All flags default to true, and may be prefixed with no- to set the flag to false instead of true. Multiple flags may be specified at once by delimiting them with spaces.

Flag
Description
loading-contentControls whether the default slot is rendered while any API caller is loading (i.e. when caller.isLoading === true).
error-contentControls whether the default slot is rendered while any API Caller is in an error state (i.e. when caller.wasSuccessful === false).
initial-contentControls whether the default slot is rendered while any API Caller has yet to receive a response for the first time (i.e. when caller.wasSuccessful === null).
initial-progressControls whether the progress indicator is shown when an API Caller is loading for the very first time (i.e. when caller.wasSuccessful === null).
secondary-progressControls whether the progress indicator is shown when an API Caller is loading any time after its first invocation (i.e. when caller.wasSuccessful !== null).
',2),A=e("p",null,"Specify if space should be reserved for the progress indicator. If set to false, the content in the default slot may jump up and down slightly as the progress indicator shows and hides.",-1),I={href:"https://vuetifyjs.com/en/components/progress-linear",target:"_blank",rel:"noopener noreferrer"},S=e("h2",{id:"slots",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#slots","aria-hidden":"true"},"#"),s(" Slots")],-1),N=e("code",null,"default",-1),q={class:"custom-container tip"},j=e("p",{class:"custom-container-title"},"TIP",-1),V={href:"https://v2.vuejs.org/v2/guide/components-slots.html#Scoped-Slots",target:"_blank",rel:"noopener noreferrer"},$=e("code",null,"#default",-1),L=e("code",null,"v-slot:default",-1),T=e("code",null,"c-loader-status",-1);function R(B,M){const l=t("RouterLink"),o=t("ExternalLinkIcon"),r=t("router-link"),p=t("Prop");return D(),y("div",null,[h,c(" MARKER:summary "),e("p",null,[s("A component for displaying progress and error information for one or more "),n(l,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:a(()=>[s("API Callers")]),_:1}),s(".")]),e("div",m,[v,e("p",null,[s("It is highly recommended that all "),n(l,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:a(()=>[s("API Callers")]),_:1}),s(" utilized by your application that don't have any other kind of error handling should be represented by a "),n(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html"},{default:a(()=>[s("c-loader-status")]),_:1}),s(" so that users can be aware of any errors that occur.")])]),c(" MARKER:summary-end "),e("p",null,[s("Progress is indicated with a "),e("a",f,[s("Vuetify"),n(o)]),s(),e("a",C,[s("v-progress-linear"),n(o)]),s(" component, and errors are displayed in a "),e("a",b,[s("v-alert"),n(o)]),s(". "),e("a",g,[s("Transitions"),n(o)]),s(" are applied to smoothly fade between the different states the the caller can be in.")]),e("div",_,[E,e("p",null,[s('This component uses the legacy term "loader" to refer to '),n(l,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:a(()=>[s("API Callers")]),_:1}),s(". A new "),w,s(" component may be coming in the future with a few usability improvements - if that happens, "),F,s(" will be preserved for backwards compatibility.")])]),e("nav",k,[e("ul",null,[e("li",null,[n(r,{to:"#examples"},{default:a(()=>[s("Examples")]),_:1})]),e("li",null,[n(r,{to:"#props"},{default:a(()=>[s("Props")]),_:1})]),e("li",null,[n(r,{to:"#slots"},{default:a(()=>[s("Slots")]),_:1})])])]),x,n(p,{def:"loaders: { [flags: string]: ApiCaller | ApiCaller[] }",lang:"ts"}),e("p",null,[s("A dictionary object with entries mapping zero or more flags to one or more "),n(l,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:a(()=>[s("API Callers")]),_:1}),s(". Multiple entries of flags/caller pairs may be specified in the dictionary to give different behavior to different API callers.")]),P,n(p,{def:"progressPlaceholder: boolean = true",lang:"ts"}),A,n(p,{def:"height: number = 10",lang:"ts"}),e("p",null,[s("Specifies the height in pixels of the "),e("a",I,[s("v-progress-linear"),n(o)]),s(" used to indicate progress.")]),S,e("p",null,[N,s(" - Accepts the content whose visibility is controlled by the state of the supplied "),n(l,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:a(()=>[s("API Callers")]),_:1}),s(". It will be shown or hidden according to the flags defined for each caller.")]),e("div",q,[j,e("p",null,[s("(Vue 2 Only): Define the default slot as a "),e("a",V,[s("scoped slot"),n(o)]),s(" (e.g. with "),$,s(" or "),L,s(" on the "),T,s(") to prevent the VNode tree from being created when the content should be hidden. This improves performance and helps avoid null reference errors that can be caused when trying to render objects that haven't been loaded yet.")])])])}const W=d(u,[["render",R],["__file","c-loader-status.html.vue"]]);export{W as default}; diff --git a/assets/c-loader-status.html.4560dc1b.js b/assets/c-loader-status.html.4560dc1b.js new file mode 100644 index 000000000..91bf9e4a3 --- /dev/null +++ b/assets/c-loader-status.html.4560dc1b.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ad853c14","path":"/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html","title":"c-loader-status","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Slots","slug":"slots","link":"#slots","children":[]}],"git":{"updatedTime":1677795733000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-loader-status.md"}');export{e as data}; diff --git a/assets/c-select-many-to-many.html.3b1f5127.js b/assets/c-select-many-to-many.html.3b1f5127.js new file mode 100644 index 000000000..ed9a8efe6 --- /dev/null +++ b/assets/c-select-many-to-many.html.3b1f5127.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5c295e24","path":"/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html","title":"c-select-many-to-many","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Events","slug":"events","link":"#events","children":[]}],"git":{"updatedTime":1694448005000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md"}');export{e as data}; diff --git a/assets/c-select-many-to-many.html.4b1a2c18.js b/assets/c-select-many-to-many.html.4b1a2c18.js new file mode 100644 index 000000000..8816f6a33 --- /dev/null +++ b/assets/c-select-many-to-many.html.4b1a2c18.js @@ -0,0 +1,13 @@ +import{_ as i,y as p,z as u,W as d,X as s,B as e,Q as n,$ as a,a5 as r,P as c}from"./framework.fe9a73df.js";const m={},y=s("h1",{id:"c-select-many-to-many",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-select-many-to-many","aria-hidden":"true"},"#"),e(" c-select-many-to-many")],-1),h=s("code",null,"/list",-1),D={class:"custom-container tip"},v=s("p",{class:"custom-container-title"},"TIP",-1),_={class:"table-of-contents"},f=r(`

Examples

<c-select-many-to-many :model="case" for="caseProducts" />
+
<c-select-many-to-many 
+    :model="case" 
+    for="caseProducts" 
+    dense
+    outlined
+/>
+
<c-select-many-to-many 
+    v-model="case.caseProducts" 
+    for="Case.caseProducts" 
+/>
+

Props

`,5),b=s("p",null,"A metadata specifier for the value being bound. One of:",-1),g=s("ul",null,[s("li",null,[e("A string with the name of the value belonging to "),s("code",null,"model"),e(".")]),s("li",null,"A direct reference to a metadata object."),s("li",null,"A string in dot-notation that starts with a type name.")],-1),C={class:"custom-container tip"},E=s("p",{class:"custom-container-title"},"Note",-1),w=s("p",null,[e("An object owning the value that was specified by the "),s("code",null,"for"),e(" prop. If provided, the input will be bound to the corresponding property on the "),s("code",null,"model"),e(" object.")],-1),x=s("p",null,[e("If binding the component with "),s("code",null,"v-model"),e(", accepts the "),s("code",null,"value"),e(" part of "),s("code",null,"v-model"),e(".")],-1),k=s("h2",{id:"events",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#events","aria-hidden":"true"},"#"),e(" Events")],-1),F=s("code",null,"model",-1),P=r("
  • adding - Fired when a new item has been selected, but before the call to /save has completed.
  • added - Fired when the call to /save has completed after adding a new item.
  • deleting - Fired when an item has been removed, but before the call to /delete has completed.
  • deleted - Fired when the call to /delete has completed after removing an item.
",1);function q(A,M){const o=c("RouterLink"),t=c("router-link"),l=c("Prop");return p(),u("div",null,[y,d(" MARKER:summary "),s("p",null,[e("A multi-select dropdown component that allows for selecting values fetched from the generated "),h,e(" API endpoints for collection navigation properties that were annotated with "),n(o,{to:"/modeling/model-components/attributes/many-to-many.html"},{default:a(()=>[e("[ManyToMany]")]),_:1}),e(".")]),d(" MARKER:summary-end "),s("div",D,[v,s("p",null,[e("It is unlikely that you'll ever need to use this component directly - it is highly recommended that you use "),n(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:a(()=>[e("c-input")]),_:1}),e(" instead and let it delegate to "),n(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html"},{default:a(()=>[e("c-select-many-to-many")]),_:1}),e(" for you.")])]),s("nav",_,[s("ul",null,[s("li",null,[n(t,{to:"#examples"},{default:a(()=>[e("Examples")]),_:1})]),s("li",null,[n(t,{to:"#props"},{default:a(()=>[e("Props")]),_:1})]),s("li",null,[n(t,{to:"#events"},{default:a(()=>[e("Events")]),_:1})])])]),f,n(l,{def:"for: string | Property | Value",lang:"ts"}),b,g,s("div",C,[E,s("p",null,[e('c-select-many-to-many expects metadata for the "real" collection navigation property on a model. If you provide it the string you passed to '),n(o,{to:"/modeling/model-components/attributes/many-to-many.html"},{default:a(()=>[e("[ManyToMany]")]),_:1}),e(", an error wil be thrown.")])]),n(l,{def:"model?: Model",lang:"ts"}),w,n(l,{def:`value?: any // Vue 2 +modelValue?: any // Vue 3`,lang:"ts"}),x,n(l,{def:"params?: ListParameters",lang:"ts"}),s("p",null,[e("An optional set of "),n(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:a(()=>[e("Data Source Standard Parameters")]),_:1}),e(" to pass to API calls made to the server.")]),k,s("p",null,[e("The following events and automatic API calls are only used when bound to a "),F,e(" that has "),n(o,{to:"/stacks/vue/layers/viewmodels.html#auto-save"},{default:a(()=>[e("auto-saves")]),_:1}),e(" enabled.")]),P])}const I=i(m,[["render",q],["__file","c-select-many-to-many.html.vue"]]);export{I as default}; diff --git a/assets/c-select-string-value.html.1351504b.js b/assets/c-select-string-value.html.1351504b.js new file mode 100644 index 000000000..2efb9af9d --- /dev/null +++ b/assets/c-select-string-value.html.1351504b.js @@ -0,0 +1,30 @@ +import{_ as r,y as D,z as i,W as c,X as s,Q as l,$ as e,B as n,a5 as d,P as o}from"./framework.fe9a73df.js";const y={},u=s("h1",{id:"c-select-string-value",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-select-string-value","aria-hidden":"true"},"#"),n(" c-select-string-value")],-1),v=s("p",null,"A dropdown component that will present a list of suggested string values from a custom API endpoint. Allows users to input values that aren't provided by the endpoint.",-1),m=s("p",null,"Effectively, this is a server-driven autocomplete list.",-1),h={class:"table-of-contents"},C=d(`

Examples

<c-select-string-value 
+    :model="person" 
+    for="jobTitle"
+    method="getSuggestedJobTitles"
+/>
+
<c-select-string-value 
+    v-model="title"
+    label="Job Title"
+    for="Person"
+    method="getSuggestedJobTitles"
+/>
+
class Person 
+{
+    public int PersonId { get; set; } 
+
+    public string JobTitle { get; set; }
+
+    [Coalesce]
+    public static async Task<ICollection<string>> GetSuggestedJobTitles(AppDbContext db, string search) 
+    {
+        return await db.People
+            .Select(p => p.JobTitle)
+            .Distinct()
+            .Where(t => t.StartsWith(search))
+            .OrderBy(t => t)
+            .Take(100)
+            .ToListAsync()
+    }
+}
+

Props

`,5),b=s("p",null,"A metadata specifier for the value being bound. One of:",-1),g=s("ul",null,[s("li",null,[n("A string with the name of the value belonging to "),s("code",null,"model"),n(".")]),s("li",null,"A direct reference to a metadata object."),s("li",null,"A string in dot-notation that starts with a type name.")],-1),f=s("p",null,[n("An object owning the value that was specified by the "),s("code",null,"for"),n(" prop. If provided, the input will be bound to the corresponding property on the "),s("code",null,"model"),n(" object.")],-1),E=s("code",null,"search",-1),_=s("code",null,"model",-1),A=s("p",null,"True if the method should be invoked and the list displayed when the entered search term is blank.",-1),F=s("p",null,"True if the bound value should be updated as the user types. Otherwise, the bound value is updated when focus is lost or when a suggested value is chosen. This is only applicable for Vuetify 2 - in Vuetify 3, this is the default behavior.",-1);function k(x,T){const t=o("router-link"),a=o("Prop"),p=o("RouterLink");return D(),i("div",null,[u,c(" MARKER:summary "),v,m,c(" MARKER:summary-end "),s("nav",h,[s("ul",null,[s("li",null,[l(t,{to:"#examples"},{default:e(()=>[n("Examples")]),_:1})]),s("li",null,[l(t,{to:"#props"},{default:e(()=>[n("Props")]),_:1})])])]),C,l(a,{def:"for: string | Property | Value",lang:"ts"}),b,g,l(a,{def:"model: Model",lang:"ts"}),f,l(a,{def:"method: string",lang:"ts"}),s("p",null,[n("The camel-cased name of the "),l(p,{to:"/modeling/model-components/methods.html"},{default:e(()=>[n("Custom Method")]),_:1}),n(" to invoke to get the list of valid values. Will be passed a single string parameter "),E,n(". Must be a static method on the type of the provided "),_,n(" object that returns a collection of strings.")]),l(a,{def:"params?: DataSourceParameters",lang:"ts"}),s("p",null,[n("An optional set of "),l(p,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:e(()=>[n("Data Source Standard Parameters")]),_:1}),n(" to pass to API calls made to the server.")]),l(a,{def:"listWhenEmpty?: boolean = false",lang:"ts"}),A,l(a,{def:"eager?: boolean = false",lang:"ts"}),F])}const w=r(y,[["render",k],["__file","c-select-string-value.html.vue"]]);export{w as default}; diff --git a/assets/c-select-string-value.html.92fa2e4d.js b/assets/c-select-string-value.html.92fa2e4d.js new file mode 100644 index 000000000..8d3b9a021 --- /dev/null +++ b/assets/c-select-string-value.html.92fa2e4d.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0227e47c","path":"/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.html","title":"c-select-string-value","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1665011516000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.md"}');export{e as data}; diff --git a/assets/c-select-values.html.962e342c.js b/assets/c-select-values.html.962e342c.js new file mode 100644 index 000000000..58ed4bfaf --- /dev/null +++ b/assets/c-select-values.html.962e342c.js @@ -0,0 +1,6 @@ +import{_ as r,y as p,z as d,W as i,X as e,B as s,Q as t,$ as n,a5 as u,P as a}from"./framework.fe9a73df.js";const m={},h=e("h1",{id:"c-select-values",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#c-select-values","aria-hidden":"true"},"#"),s(" c-select-values")],-1),_=e("p",null,"A multi-select input component for collections of non-object values (primarily strings and numbers).",-1),D={class:"custom-container tip"},v=e("p",{class:"custom-container-title"},"TIP",-1),y={class:"table-of-contents"},f=u(`

Examples

<c-select-values 
+    :model="post.setTags.args" 
+    for="Post.methods.setTags.params.tagNames" 
+/>
+

Props

`,3),g=e("p",null,"A metadata specifier for the value being bound. One of:",-1),b=e("ul",null,[e("li",null,[s("A string with the name of the value belonging to "),e("code",null,"model"),s(".")]),e("li",null,"A direct reference to a metadata object."),e("li",null,"A string in dot-notation that starts with a type name.")],-1),C=e("p",null,[s("An object owning the value that was specified by the "),e("code",null,"for"),s(" prop.")],-1),x=e("p",null,[s("If binding the component with "),e("code",null,"v-model"),s(", accepts the "),e("code",null,"value"),s(" part of "),e("code",null,"v-model"),s(".")],-1);function E(k,V){const o=a("RouterLink"),c=a("router-link"),l=a("Prop");return p(),d("div",null,[h,i(" MARKER:summary "),_,i(" MARKER:summary-end "),e("div",D,[v,e("p",null,[s("It is unlikely that you'll ever need to use this component directly - it is highly recommended that you use "),t(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:n(()=>[s("c-input")]),_:1}),s(" instead and let it delegate to "),t(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html"},{default:n(()=>[s("c-select-values")]),_:1}),s(" for you.")])]),e("nav",y,[e("ul",null,[e("li",null,[t(c,{to:"#examples"},{default:n(()=>[s("Examples")]),_:1})]),e("li",null,[t(c,{to:"#props"},{default:n(()=>[s("Props")]),_:1})])])]),f,t(l,{def:"for: string | CollectionProperty | CollectionValue",lang:"ts"}),g,b,t(l,{def:"model?: Model",lang:"ts"}),C,t(l,{def:`value?: any // Vue 2 +modelValue?: any // Vue 3`,lang:"ts"}),x])}const P=r(m,[["render",E],["__file","c-select-values.html.vue"]]);export{P as default}; diff --git a/assets/c-select-values.html.e35cce64.js b/assets/c-select-values.html.e35cce64.js new file mode 100644 index 000000000..501c7865a --- /dev/null +++ b/assets/c-select-values.html.e35cce64.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1cab422f","path":"/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html","title":"c-select-values","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-select-values.md"}');export{e as data}; diff --git a/assets/c-select.html.44d765fd.js b/assets/c-select.html.44d765fd.js new file mode 100644 index 000000000..d69b00db8 --- /dev/null +++ b/assets/c-select.html.44d765fd.js @@ -0,0 +1,39 @@ +import{_ as i,y as D,z as d,W as r,X as s,Q as n,$ as a,B as e,a5 as c,P as p}from"./framework.fe9a73df.js";const y={},u=s("h1",{id:"c-select",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#c-select","aria-hidden":"true"},"#"),e(" c-select")],-1),h=s("p",null,[e("A dropdown component that allows for selecting values fetched from the generated "),s("code",null,"/list"),e(" API endpoints.")],-1),m=s("p",null,"Used both for selecting values for foreign key and navigation properties, and for selecting arbitrary objects or primary keys independent of a parent or owning object.",-1),C={class:"table-of-contents"},v=c(`

Examples

Binding to a navigation property or foreign key of a model:

  <c-select :model="person" for="company" />
+  <!-- OR: -->
+  <c-select :model="person" for="companyId" />
+

Binding an arbitrary primary key value or an arbitrary object:

  <!-- Binding a key: -->
+  <c-select for="Person" :key-value.sync="selectedPersonId" />
+
+  <!-- Binding an object: -->
+  <c-select for="Person" :object-value.sync="selectedPerson" />
+  <c-select for="Person" v-model="selectedPerson" />
+

Examples of other props:

<c-select 
+  for="Person" 
+  v-model="selectedPerson"
+  :clearable="false"
+  preselect-first
+  :params="{ pageSize: 42, filter: { isActive: true } }"
+  :create="createMethods"
+  dense
+  outlined
+  color="pink"
+/>
+<!-- \`createMethods\` is defined in the docs of \`create\` below -->
+

Props

`,8),f=s("p",null,"A metadata specifier for the value being bound. One of:",-1),b=s("ul",null,[s("li",null,[e("The name of a foreign key or reference navigation property belonging to "),s("code",null,"model"),e(".")]),s("li",null,"The name of a model type."),s("li",null,"A direct reference to a metadata object."),s("li",null,"A string in dot-notation that starts with a type name that resolves to a foreign key or reference navigation property.")],-1),g=s("div",{class:"custom-container tip"},[s("p",{class:"custom-container-title"},"TIP"),s("p",null,"When binding by a key value, if the corresponding object cannot be found (e.g. there is no navigation property, or the navigation property is null), c-select will automatically attempt to load the object from the server so it can be displayed in the UI.")],-1),_=s("p",null,[e("An object owning the value that was specified by the "),s("code",null,"for"),e(" prop. If provided, the input will be bound to the corresponding property on the "),s("code",null,"model"),e(" object.")],-1),E=s("p",null,[e("If "),s("code",null,"for"),e(" specifies a foreign key or reference navigation property, both the foreign key and the navigation property of the "),s("code",null,"model"),e(" will be updated when the selected value is changed.")],-1),w=s("p",null,[e("When binding the component with "),s("code",null,"v-model"),e(", accepts the "),s("code",null,"value"),e(" part of "),s("code",null,"v-model"),e(". If "),s("code",null,"for"),e(" was specified as a foreign key, this will expect a key; likewise, if "),s("code",null,"for"),e(" was specified as a type or as a navigation property, this will expect an object.")],-1),F=s("p",null,[e("When bound with "),s("code",null,':key-value.sync="keyValue"'),e(", allows binding the primary key of the selected object explicitly.")],-1),k=s("p",null,[e("When bound with "),s("code",null,':object-value.sync="objectValue"'),e(", allows binding the selected object explicitly.")],-1),A=s("p",null,[e("Whether the selection can be cleared or not, emitting "),s("code",null,"null"),e(" as the input value.")],-1),q=s("code",null,"required",-1),x=s("p",null,"If true, then when the first list results for the component are received by the client just after the component is created, c-select will emit the first item in the list as the selected value.",-1),P=s("p",null,"If true, then when the first list results for the component are received by the client just after the component is created, if the results contained exactly one item, c-select will emit that only item as the selected value.",-1),j=s("p",null,[e("If true, the list results will be reloaded when the dropdown menu is opened. By default, list results are loaded when the component is mounted and also when any of its parameters change (either search input or the "),s("code",null,"params"),e(" prop).")],-1),I=s("p",null,"A object containing a pair of methods that allowing users to create new items from directly within the c-select if a matching object is not found.",-1),B=s("p",null,[e("The object must contain the following two methods. You should define these in your component's "),s("code",null,"script"),e(" section - don't try to define them inline in your component.")],-1),L={style:{"margin-left":"20px"}},M=s("p",null,"A function that will be called with the user's current search term, as well as the collection of currently loaded items being presented to the user as valid selection options.",-1),S=s("p",null,[e("It should return either a "),s("code",null,"string"),e(" that will be presented to the user as an option in the dropdown that can be clicked to invoke the "),s("code",null,"getItem"),e(" function below, or it should return "),s("code",null,"false"),e(" to prevent such an option from being shown to the user.")],-1),V=s("p",null,[e("A function that will be invoked when the user clicks the option in the dropdown list described by "),s("code",null,"getLabel"),e(". It will be given the user's current search term as well as the value of the label returned from "),s("code",null,"getLabel"),e(" as parameters. It must perform the necessary operations to create the new object on the server and then return a reference to that object.")],-1),T=c(`

For example:

createMethods = {
+  getLabel(search: string, items: Person[]) {
+    const searchLower = search.toLowerCase();
+    if (items.some(a => a.name?.toLowerCase().indexOf(searchLower) == 0)) {
+      return false;
+    }
+    return search;
+  },
+  async getItem(search: string, label: string) {
+    const client = new PersonApiClient();
+    return (await client.addPersonByName(label)).data.object!;
+  }
+}
+

Slots

`,3),R=s("code",null,'#item="{ item, search }"',-1),N=s("code",null,"item",-1),W=s("code",null,"search",-1),z=s("p",null,[s("code",null,'#list-item="{ item, search }"'),e(" - Slot used to customize the text of items inside the list. If not provided, falls back to the "),s("code",null,"item"),e(" slot.")],-1),O=s("p",null,[s("code",null,'#selected-item="{ item, search }"'),e(" - Slot used to customize the text of selected items. If not provided, falls back to the "),s("code",null,"item"),e(" slot.")],-1);function K(U,Q){const t=p("router-link"),l=p("Prop"),o=p("RouterLink");return D(),d("div",null,[u,r(" MARKER:summary "),h,m,r(" MARKER:summary-end "),s("nav",C,[s("ul",null,[s("li",null,[n(t,{to:"#examples"},{default:a(()=>[e("Examples")]),_:1})]),s("li",null,[n(t,{to:"#props"},{default:a(()=>[e("Props")]),_:1})]),s("li",null,[n(t,{to:"#slots"},{default:a(()=>[e("Slots")]),_:1})])])]),v,n(l,{def:"for: string | ForeignKeyProperty | ModelReferenceNavigationProperty | ModelType",lang:"ts"}),f,b,g,n(l,{def:"model?: Model",lang:"ts"}),_,E,n(l,{def:`value?: any // Vue 2 +modelValue?: any // Vue 3`,lang:"ts"}),w,n(l,{def:"keyValue?: any",lang:"ts"}),F,n(l,{def:"objectValue?: any",lang:"ts"}),k,n(l,{def:"clearable?: boolean",lang:"ts"}),A,s("p",null,[e("If not specified and the component is bound to a foreign key or reference navigation property, defaults to whether or not the foreign key has a "),q,e(" validation rule defined in its "),n(o,{to:"/stacks/vue/layers/metadata.html"},{default:a(()=>[e("Metadata")]),_:1}),e(".")]),n(l,{def:"preselectFirst?: boolean = false",lang:"ts"}),x,n(l,{def:"preselectSingle?: boolean = false",lang:"ts"}),P,n(l,{def:"reloadOnOpen?: boolean = false",lang:"ts"}),j,n(l,{def:"params?: ListParameters",lang:"ts"}),s("p",null,[e("An optional set of "),n(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:a(()=>[e("Data Source Standard Parameters")]),_:1}),e(" to pass to API calls made to the server.")]),n(l,{def:`create?: { + getLabel: (search: string, items: TModel[]) => string | false, + getItem: (search: string, label: string) => Promise +}`,lang:"ts"}),I,B,s("div",L,[n(l,{def:"create.getLabel: (search: string, items: TModel[]) => string | false",lang:"ts",id:"member-create-getLabel"}),M,S,n(l,{def:"create.getItem: (search: string, label: string) => Promise",lang:"ts",id:"member-create-getItem"}),V]),T,s("p",null,[R,e(" - Slot used to customize the text of both items inside the list, as well as the text of selected items. By default, items are rendered with "),n(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:a(()=>[e("c-display")]),_:1}),e(". Slot is passed a parameter "),N,e(" containing a "),n(o,{to:"/stacks/vue/layers/models.html"},{default:a(()=>[e("model instance")]),_:1}),e(", and "),W,e(" containing the current search query..")]),z,O])}const Y=i(y,[["render",K],["__file","c-select.html.vue"]]);export{Y as default}; diff --git a/assets/c-select.html.c5764dd7.js b/assets/c-select.html.c5764dd7.js new file mode 100644 index 000000000..bff95f92a --- /dev/null +++ b/assets/c-select.html.c5764dd7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-708bd4f4","path":"/stacks/vue/coalesce-vue-vuetify/components/c-select.html","title":"c-select","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Examples","slug":"examples","link":"#examples","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Slots","slug":"slots","link":"#slots","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-select.md"}');export{e as data}; diff --git a/assets/c-table.html.6f5fc144.js b/assets/c-table.html.6f5fc144.js new file mode 100644 index 000000000..7f8f5a053 --- /dev/null +++ b/assets/c-table.html.6f5fc144.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1244db54","path":"/stacks/vue/coalesce-vue-vuetify/components/c-table.html","title":"c-table","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Props","slug":"props","link":"#props","children":[]},{"level":2,"title":"Slots","slug":"slots","link":"#slots","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/components/c-table.md"}');export{e as data}; diff --git a/assets/c-table.html.fc5ab5de.js b/assets/c-table.html.fc5ab5de.js new file mode 100644 index 000000000..61a505e41 --- /dev/null +++ b/assets/c-table.html.fc5ab5de.js @@ -0,0 +1,19 @@ +import{_ as c,y as i,z as d,W as r,X as e,B as s,Q as n,$ as a,a5 as D,P as p}from"./framework.fe9a73df.js";const u={},y=e("h1",{id:"c-table",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#c-table","aria-hidden":"true"},"#"),s(" c-table")],-1),m={class:"table-of-contents"},h=e("h2",{id:"example-usage",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#example-usage","aria-hidden":"true"},"#"),s(" Example Usage")],-1),v=D(`
<c-table :list="list" />
+

A more complex example using more of the available options:

<c-table
+  :list="list"
+  :props="['firstName', 'lastName']"
+  :extra-headers="['Actions']"
+>
+  <template #item.append="{item}"> 
+    <td>
+      <v-btn
+        title="Edit"
+        text icon
+        :to="{name: 'edit-person', params: { id: item.$primaryKey }}"
+      >
+        <i class="fa fa-edit"></i>
+      </v-btn>
+    </td>
+  </template>
+</c-table>
+

Props

`,4),b=e("p",null,"If provided, specifies which properties, and their ordering, should be given a column in the table.",-1),C=e("p",null,[s("The text contents of one or more extra "),e("code",null,"th"),s(" elements to render in the table. Should be used in conjunction with the "),e("code",null,"item.append"),s(" slot.")],-1),f=e("code",null,"admin",-1),_=e("h2",{id:"slots",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#slots","aria-hidden":"true"},"#"),s(" Slots")],-1),g=e("p",null,[e("code",null,"item.append"),s(" - A slot rendered after the "),e("code",null,"td"),s(" elements on each row that render the properties of each item in the table. Should be provided zero or more additional "),e("code",null,"td"),s(" elements. The number should match the number of additional headers provided to the "),e("code",null,"extraHeaders"),s(" prop.")],-1);function E(x,w){const l=p("RouterLink"),t=p("router-link"),o=p("Prop");return i(),d("div",null,[y,r(" MARKER:summary "),e("p",null,[s("A table component for displaying the contents of a "),n(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[s("ListViewModel")]),_:1}),s(". Also supports modifying the list's [sort parameters](/modeling/model-components/data-sources.md#standard-parameters) by clicking on column headers. Pairs well with a "),n(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html"},{default:a(()=>[s("c-list-pagination")]),_:1}),s(".")]),r(" MARKER:summary-end "),e("nav",m,[e("ul",null,[e("li",null,[n(t,{to:"#example-usage"},{default:a(()=>[s("Example Usage")]),_:1})]),e("li",null,[n(t,{to:"#props"},{default:a(()=>[s("Props")]),_:1})]),e("li",null,[n(t,{to:"#slots"},{default:a(()=>[s("Slots")]),_:1})])])]),h,e("p",null,[s("A simple table, rendering the items of a "),n(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[s("ListViewModel")]),_:1}),s(":")]),v,n(o,{def:"list: ListViewModel",lang:"ts"}),e("p",null,[s("The "),n(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[s("ListViewModel")]),_:1}),s(" to display pagination information for.")]),n(o,{def:"props?: string[]",lang:"ts"}),b,e("p",null,[s("If not provided, all non-key columns that aren't annotated with "),n(l,{to:"/modeling/model-components/attributes/hidden.html"},{default:a(()=>[s("[Hidden(HiddenAttribute.Areas.List)]")]),_:1}),s(" are given a column.")]),n(o,{def:"extraHeaders?: string[]",lang:"ts"}),C,n(o,{def:"editable: boolean = false",lang:"ts"}),e("p",null,[s("If true, properties in each table cell will be rendered with "),n(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:a(()=>[s("c-input")]),_:1}),s(". Non-editable properties will be rendered in accordance with the value of the "),f,s(" prop.")]),n(o,{def:"admin: boolean = false",lang:"ts"}),e("p",null,[s("If true, properties in each table cell will be rendered with "),n(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html"},{default:a(()=>[s("c-admin-display")]),_:1}),s(" instead of "),n(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:a(()=>[s("c-display")]),_:1}),s(".")]),_,g])}const F=c(u,[["render",E],["__file","c-table.html.vue"]]);export{F as default}; diff --git a/assets/client-validation.html.3e200ba2.js b/assets/client-validation.html.3e200ba2.js new file mode 100644 index 000000000..6264ff11c --- /dev/null +++ b/assets/client-validation.html.3e200ba2.js @@ -0,0 +1,36 @@ +import{_ as i,y,z as C,X as s,B as l,Q as o,$ as e,a5 as c,P as a}from"./framework.fe9a73df.js";const d={},u=s("h1",{id:"clientvalidation",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#clientvalidation","aria-hidden":"true"},"#"),l(" [ClientValidation]")],-1),E=s("p",null,[l("The "),s("code",null,"[IntelliTect.Coalesce.DataAnnotations.ClientValidation]"),l(" attribute is used to control the behavior of client-side model validation and to add additional client-only validation parameters. Database validation is available via standard "),s("code",null,"System.ComponentModel.DataAnnotations"),l(" annotations.")],-1),b={href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noopener noreferrer"},h={class:"custom-container warning"},F=s("p",{class:"custom-container-title"},"WARNING",-1),v={class:"table-of-contents"},m=c(`

Example Usage

public class Person
+{
+    public int PersonId { get; set; }
+
+    [ClientValidation(IsRequired = true, AllowSave = true)]
+    public string FirstName { get; set; }
+
+    [ClientValidation(IsRequired = true, AllowSave = false, MinLength = 1, MaxLength = 100)]
+    public string LastName { get; set; }
+}
+

Properties

Behavioral Properties

`,4),g=c('

If set to true, any client validation errors on the property will not prevent saving on the client. This includes all client-side validation, including null-checking for required foreign keys and other validations that are implicit. This also includes other explicit validation from System.ComponentModel.DataAnnotations annotations.

Instead, validation errors will be treated only as warnings, and will be available through the warnings: KnockoutValidationErrors property on the TypeScript ViewModel.

Note

Use AllowSave = true to allow partially complete data to still be saved, protecting your user from data loss upon navigation while still hinting to them that they are not done filling out data.

',3),_=s("p",null,"Set an error message to be used if any client validations fail",-1),f=s("h3",{id:"validation-rule-properties",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#validation-rule-properties","aria-hidden":"true"},"#"),l(" Validation Rule Properties")],-1),k=s("div",{class:"language-c# line-numbers-mode","data-ext":"c#"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsRequired"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MinLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MaxLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"string"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Pattern"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsEmail"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsPhoneUs"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),V={href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noopener noreferrer"},x=s("div",{class:"language-c# line-numbers-mode","data-ext":"c#"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsRequired"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MinLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MaxValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"MaxLength"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; } = "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"MinValue"),s("span",{style:{color:"#D4D4D4"}},";")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"double"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Step"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"string"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Pattern"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsEmail"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsPhoneUs"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsDate"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsDateIso"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsNumber"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"bool"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"IsDigit"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),w=s("p",null,"The following attribute properties are outputted to TypeScript unquoted. If you need to assert equality to a string, wrap the value you set to this property in quotes. Other literals (numerics, booleans, etc) need no wrapping.",-1),M=s("div",{class:"language-c# line-numbers-mode","data-ext":"c#"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"string"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Equal"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"string"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"NotEqual"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),I={href:"https://github.com/Knockout-Contrib/Knockout-Validation/",target:"_blank",rel:"noopener noreferrer"},T=s("p",null,[l("It will be emitted into the TypeScript as "),s("code",null,"this.extend({ CustomName: CustomValue })"),l(". Neither value will be quoted in the emitted TypeScript - add quotes to your value as needed to generate valid TypeScript.")],-1),P=s("div",{class:"language-c# line-numbers-mode","data-ext":"c#"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"string"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"CustomName"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"public"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"string"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"CustomValue"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"get"),s("span",{style:{color:"#D4D4D4"}},"; "),s("span",{style:{color:"#9CDCFE"}},"set"),s("span",{style:{color:"#D4D4D4"}},"; }")]),l(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1);function S(N,q){const n=a("RouterLink"),D=a("ExternalLinkIcon"),t=a("router-link"),r=a("Prop"),p=a("CodeTabs");return y(),C("div",null,[u,E,s("p",null,[l("These propagate to the client as validations in TypeScript via generated "),o(n,{to:"/stacks/vue/layers/metadata.html"},{default:e(()=>[l("Metadata")]),_:1}),l(" and "),o(n,{to:"/stacks/vue/layers/viewmodels.html"},{default:e(()=>[l("ViewModel rules")]),_:1}),l(" (for Vue) or "),s("a",b,[l("Knockout-Validation"),o(D)]),l(" rules (for Knockout). For both stacks, any failing validation rules prevent saves from going to the server.")]),s("div",h,[F,s("p",null,[l("This attribute controls client-side validation only. To perform server-side validation, create a custom "),o(n,{to:"/modeling/model-components/behaviors.html"},{default:e(()=>[l("Behaviors class")]),_:1}),l(" for your types.")])]),s("nav",v,[s("ul",null,[s("li",null,[o(t,{to:"#example-usage"},{default:e(()=>[l("Example Usage")]),_:1})]),s("li",null,[o(t,{to:"#properties"},{default:e(()=>[l("Properties")]),_:1}),s("ul",null,[s("li",null,[o(t,{to:"#behavioral-properties"},{default:e(()=>[l("Behavioral Properties")]),_:1})]),s("li",null,[o(t,{to:"#validation-rule-properties"},{default:e(()=>[l("Validation Rule Properties")]),_:1})])])])])]),m,o(r,{def:"public bool AllowSave { get; set; } // Knockout Only"}),g,o(r,{def:"public string ErrorMessage { get; set; }"}),_,f,o(p,null,{vue:e(()=>[s("p",null,[l("In addition to the following properties, you also customize validation on a per-instance basis of the "),o(n,{to:"/stacks/vue/layers/viewmodels.html#viewmodels"},{default:e(()=>[l("ViewModels")]),_:1}),l(" using the "),o(n,{to:"/stacks/vue/layers/viewmodels.html#rules-validation"},{default:e(()=>[l("Rules/Validation")]),_:1}),l(" methods.")]),k]),knockout:e(()=>[s("p",null,[l("The following attribute properties all map directly to "),s("a",V,[l("Knockout-Validation"),o(D)]),l(" properties.")]),x,w,M,s("p",null,[l("The following two properties may be used together to specify a custom "),s("a",I,[l("Knockout-Validation"),o(D)]),l(" property.")]),T,P]),_:1})])}const K=i(d,[["render",S],["__file","client-validation.html.vue"]]);export{K as default}; diff --git a/assets/client-validation.html.64bb4309.js b/assets/client-validation.html.64bb4309.js new file mode 100644 index 000000000..0068814c0 --- /dev/null +++ b/assets/client-validation.html.64bb4309.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f401e784","path":"/modeling/model-components/attributes/client-validation.html","title":"[ClientValidation]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[{"level":3,"title":"Behavioral Properties","slug":"behavioral-properties","link":"#behavioral-properties","children":[]},{"level":3,"title":"Validation Rule Properties","slug":"validation-rule-properties","link":"#validation-rule-properties","children":[]}]}],"git":{"updatedTime":1654915632000},"filePathRelative":"modeling/model-components/attributes/client-validation.md"}');export{e as data}; diff --git a/assets/clientConfigs.922cb10d.js b/assets/clientConfigs.922cb10d.js new file mode 100644 index 000000000..1cbc3873d --- /dev/null +++ b/assets/clientConfigs.922cb10d.js @@ -0,0 +1,17 @@ +import{u as lt,r as _a,a as de,b as $e,c as oo,d as hn,w as ao,C as io,_ as Ln,e as ga}from"./app.09d0ccf5.js";import{c as R,r as Y,h as I,o as _e,j as U,T as _n,x as qe,k as gn,y as S,z as P,A as M,B as Be,C as z,D as ce,E as co,_ as q,F as ya,G as b,a as ba,w as Ie,H as Oa,I as Sa,J as wa,K as yn,L as ka,M as Ea,N as so,v as lo,O as Pa,i as $t,e as Ht,t as uo,u as je,g as Ia,P as he,Q as H,U as ie,V as Oe,W as V,X as T,Y as bn,Z as J,$ as Z,a0 as Nn,l as Bt,a1 as ja,a2 as Ca,a3 as Pt,a4 as It,m as Da,q as xa,a5 as La,a6 as Na,a7 as Aa,d as An,a8 as Tn}from"./framework.fe9a73df.js";var Fe=(e={})=>e;function fo(e,t,n){var r,o,a;t===void 0&&(t=50),n===void 0&&(n={});var i=(r=n.isImmediate)!=null&&r,c=(o=n.callback)!=null&&o,l=n.maxWait,u=Date.now(),s=[];function p(){if(l!==void 0){var m=Date.now()-u;if(m+t>=l)return l-m}return t}var d=function(){var m=[].slice.call(arguments),g=this;return new Promise(function(v,h){var _=i&&a===void 0;if(a!==void 0&&clearTimeout(a),a=setTimeout(function(){if(a=void 0,u=Date.now(),!i){var O=e.apply(g,m);c&&c(O),s.forEach(function(y){return(0,y.resolve)(O)}),s=[]}},p()),_){var w=e.apply(g,m);return c&&c(w),v(w)}s.push({resolve:v,reject:h})})};return d.cancel=function(m){a!==void 0&&clearTimeout(a),s.forEach(function(g){return(0,g.reject)(m)}),s=[]},d}const Rn=()=>window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,Ta=()=>window.scrollTo({top:0,behavior:"smooth"});const Ra=R({name:"BackToTop",setup(){const e=Y(0),t=I(()=>e.value>300),n=fo(()=>{e.value=Rn()},100);_e(()=>{e.value=Rn(),window.addEventListener("scroll",()=>n())});const r=U("div",{class:"back-to-top",onClick:Ta});return()=>U(_n,{name:"back-to-top"},()=>t.value?r:null)}}),Ma=Fe({rootComponents:[Ra]});const $a=U("svg",{class:"external-link-icon",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",x:"0px",y:"0px",viewBox:"0 0 100 100",width:"15",height:"15"},[U("path",{fill:"currentColor",d:"M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"}),U("polygon",{fill:"currentColor",points:"45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"})]),Ha=R({name:"ExternalLinkIcon",props:{locales:{type:Object,required:!1,default:()=>({})}},setup(e){const t=lt(),n=I(()=>{var r;return(r=e.locales[t.value])!=null?r:{openInNewWindow:"open in new window"}});return()=>U("span",[$a,U("span",{class:"external-link-icon-sr-only"},n.value.openInNewWindow)])}}),Ba={"/":{openInNewWindow:"open in new window"}},qa=Fe({enhance({app:e}){e.component("ExternalLinkIcon",U(Ha,{locales:Ba}))}});/** + * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT + */const L={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
'},status:null,set:e=>{const t=L.isStarted();e=Wt(e,L.settings.minimum,1),L.status=e===1?null:e;const n=L.render(!t),r=n.querySelector(L.settings.barSelector),o=L.settings.speed,a=L.settings.easing;return n.offsetWidth,Fa(i=>{dt(r,{transform:"translate3d("+Mn(e)+"%,0,0)",transition:"all "+o+"ms "+a}),e===1?(dt(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){dt(n,{transition:"all "+o+"ms linear",opacity:"0"}),setTimeout(function(){L.remove(),i()},o)},o)):setTimeout(()=>i(),o)}),L},isStarted:()=>typeof L.status=="number",start:()=>{L.status||L.set(0);const e=()=>{setTimeout(()=>{!L.status||(L.trickle(),e())},L.settings.trickleSpeed)};return L.settings.trickle&&e(),L},done:e=>!e&&!L.status?L:L.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=L.status;return t?(typeof e!="number"&&(e=(1-t)*Wt(Math.random()*t,.1,.95)),t=Wt(t+e,0,.994),L.set(t)):L.start()},trickle:()=>L.inc(Math.random()*L.settings.trickleRate),render:e=>{if(L.isRendered())return document.getElementById("nprogress");$n(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=L.settings.template;const n=t.querySelector(L.settings.barSelector),r=e?"-100":Mn(L.status||0),o=document.querySelector(L.settings.parent);return dt(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),o!==document.body&&$n(o,"nprogress-custom-parent"),o==null||o.appendChild(t),t},remove:()=>{Hn(document.documentElement,"nprogress-busy"),Hn(document.querySelector(L.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&Ua(e)},isRendered:()=>!!document.getElementById("nprogress")},Wt=(e,t,n)=>en?n:e,Mn=e=>(-1+e)*100,Fa=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),dt=function(){const e=["Webkit","O","Moz","ms"],t={};function n(i){return i.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(c,l){return l.toUpperCase()})}function r(i){const c=document.body.style;if(i in c)return i;let l=e.length;const u=i.charAt(0).toUpperCase()+i.slice(1);let s;for(;l--;)if(s=e[l]+u,s in c)return s;return i}function o(i){return i=n(i),t[i]||(t[i]=r(i))}function a(i,c,l){c=o(c),i.style[c]=l}return function(i,c){for(const l in c){const u=c[l];u!==void 0&&Object.prototype.hasOwnProperty.call(c,l)&&a(i,l,u)}}}(),po=(e,t)=>(typeof e=="string"?e:On(e)).indexOf(" "+t+" ")>=0,$n=(e,t)=>{const n=On(e),r=n+t;po(n,t)||(e.className=r.substring(1))},Hn=(e,t)=>{const n=On(e);if(!po(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},On=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),Ua=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)};const Va=()=>{_e(()=>{const e=qe(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||L.start()}),e.afterEach(n=>{t.add(n.path),L.done()})})},Wa=Fe({setup(){Va()}}),Ka=JSON.parse(`{"contributors":false,"repo":"intellitect/coalesce","docsDir":"docs","docsBranch":"dev","sidebar":[{"text":"Introduction","link":"/","children":["/stacks/vue/getting-started",{"text":"Generated Code","link":"/stacks/agnostic/generation","collapsible":true,"children":["/stacks/agnostic/dtos"]}]},{"text":"Model Types","children":["/modeling/model-types/entities","/modeling/model-types/external-types","/modeling/model-types/dtos","/modeling/model-types/services"]},{"text":"Model Components","children":["/topics/security","/modeling/model-components/properties",{"text":"Attributes","link":"/modeling/model-components/attributes.html","collapsible":true,"children":["/modeling/model-components/attributes/client-validation.md","/modeling/model-components/attributes/coalesce.md","/modeling/model-components/attributes/controller-action.md","/modeling/model-components/attributes/controller.md","/modeling/model-components/attributes/create-controller.md","/modeling/model-components/attributes/date-type.md","/modeling/model-components/attributes/default-order-by.md","/modeling/model-components/attributes/dto-includes-excludes.md","/modeling/model-components/attributes/execute.md","/modeling/model-components/attributes/hidden.md","/modeling/model-components/attributes/inject.md","/modeling/model-components/attributes/internal-use.md","/modeling/model-components/attributes/list-text.md","/modeling/model-components/attributes/load-from-data-source.md","/modeling/model-components/attributes/many-to-many.md","/modeling/model-components/attributes/search.md","/modeling/model-components/attributes/security-attribute.md","/modeling/model-components/attributes/select-filter.md","/modeling/model-components/attributes/typescript-partial.md"]},"/modeling/model-components/methods","/modeling/model-components/data-sources","/modeling/model-components/behaviors"]},{"text":"Vue","children":[{"text":"Overview","link":"/stacks/vue/overview"},{"text":"Metadata","link":"/stacks/vue/layers/metadata"},{"text":"Models","link":"/stacks/vue/layers/models"},{"text":"API Clients","link":"/stacks/vue/layers/api-clients"},{"text":"View Models","link":"/stacks/vue/layers/viewmodels"},{"text":"Vue 2 to Vue 3","link":"/stacks/vue/vue2-to-vue3"}]},{"text":"Vuetify Components","collapsible":true,"children":[{"text":"Overview","link":"/stacks/vue/coalesce-vue-vuetify/overview"},"/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.md","/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.md","/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.md","/stacks/vue/coalesce-vue-vuetify/components/c-display.md","/stacks/vue/coalesce-vue-vuetify/components/c-input.md","/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.md","/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.md","/stacks/vue/coalesce-vue-vuetify/components/c-list-page.md","/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.md","/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.md","/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.md","/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.md","/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.md","/stacks/vue/coalesce-vue-vuetify/components/c-select-values.md","/stacks/vue/coalesce-vue-vuetify/components/c-select.md","/stacks/vue/coalesce-vue-vuetify/components/c-table.md"]},{"text":"Concepts","children":["/concepts/include-tree","/concepts/includes"]},{"text":"Configuration","children":["/topics/startup","/topics/coalesce-json"]},{"text":"Knockout (legacy)","collapsible":true,"children":["/stacks/ko/overview","/stacks/ko/getting-started","/stacks/ko/client/view-model","/stacks/ko/client/list-view-model","/stacks/ko/client/external-view-model","/stacks/ko/client/methods","/stacks/ko/client/model-config","/stacks/ko/client/bindings"]}],"locales":{"/":{"selectLanguageName":"English"}},"colorMode":"auto","colorModeSwitch":true,"navbar":[],"logo":null,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebarDepth":2,"editLink":true,"editLinkText":"Edit this page","lastUpdated":true,"lastUpdatedText":"Last Updated","contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),za=Y(Ka),Ja=()=>za,mo=Symbol(""),Qa=()=>{const e=gn(mo);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},Ga=(e,t)=>{var n;return{...e,...(n=e.locales)==null?void 0:n[t]}},Za=Fe({enhance({app:e}){const t=Ja(),n=e._context.provides[_a],r=I(()=>Ga(t.value,n.value));e.provide(mo,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),Ya=R({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(S(),P("span",{class:ce(["badge",e.type]),style:co({verticalAlign:e.vertical})},[M(t.$slots,"default",{},()=>[Be(z(e.text),1)])],6))}}),Xa=q(Ya,[["__file","Badge.vue"]]),ei=R({name:"CodeGroup",setup(e,{slots:t}){const n=Y(-1),r=Y([]),o=(c=n.value)=>{c{c>0?n.value=c-1:n.value=r.value.length-1,r.value[n.value].focus()},i=(c,l)=>{c.key===" "||c.key==="Enter"?(c.preventDefault(),n.value=l):c.key==="ArrowRight"?(c.preventDefault(),o(l)):c.key==="ArrowLeft"&&(c.preventDefault(),a(l))};return()=>{var l;const c=(((l=t.default)==null?void 0:l.call(t))||[]).filter(u=>u.type.name==="CodeGroupItem").map(u=>(u.props===null&&(u.props={}),u));return c.length===0?null:(n.value<0||n.value>c.length-1?(n.value=c.findIndex(u=>u.props.active===""||u.props.active===!0),n.value===-1&&(n.value=0)):c.forEach((u,s)=>{u.props.active=s===n.value}),U("div",{class:"code-group"},[U("div",{class:"code-group__nav"},U("ul",{class:"code-group__ul"},c.map((u,s)=>{const p=s===n.value;return U("li",{class:"code-group__li"},U("button",{ref:d=>{d&&(r.value[s]=d)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":p},ariaPressed:p,ariaExpanded:p,onClick:()=>n.value=s,onKeydown:d=>i(d,s)},u.props.title))}))),c]))}}}),ti=["aria-selected"],ni=R({name:"CodeGroupItem"}),ri=R({...ni,props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(S(),P("div",{class:ce(["code-group-item",{"code-group-item__active":e.active}]),"aria-selected":e.active},[M(t.$slots,"default")],10,ti))}}),oi=q(ri,[["__file","CodeGroupItem.vue"]]);var Bn;const vo=typeof window<"u",ai=e=>typeof e=="function",ii=e=>typeof e=="string",ci=()=>{};vo&&((Bn=window==null?void 0:window.navigator)==null?void 0:Bn.userAgent)&&/iP(ad|hone|od)/.test(window.navigator.userAgent);function ot(e){return typeof e=="function"?e():b(e)}function si(e,t){function n(...r){return new Promise((o,a)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(a)})}return n}const ho=e=>e();function li(e=ho){const t=Y(!0);function n(){t.value=!1}function r(){t.value=!0}const o=(...a)=>{t.value&&e(...a)};return{isActive:ba(t),pause:n,resume:r,eventFilter:o}}function ui(e){return e}function _o(e){return Oa()?(Sa(e),!0):!1}function fi(e){return typeof e=="function"?I(e):Y(e)}function pi(e,t=!0){wa()?_e(e):t?e():yn(e)}function di(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,o=ya(e),a=Y(e);function i(c){if(arguments.length)return a.value=c,a.value;{const l=ot(n);return a.value=a.value===l?ot(r):l,a.value}}return o?i:[a,i]}var qn=Object.getOwnPropertySymbols,mi=Object.prototype.hasOwnProperty,vi=Object.prototype.propertyIsEnumerable,hi=(e,t)=>{var n={};for(var r in e)mi.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&qn)for(var r of qn(e))t.indexOf(r)<0&&vi.call(e,r)&&(n[r]=e[r]);return n};function _i(e,t,n={}){const r=n,{eventFilter:o=ho}=r,a=hi(r,["eventFilter"]);return Ie(e,si(o,t),a)}var gi=Object.defineProperty,yi=Object.defineProperties,bi=Object.getOwnPropertyDescriptors,jt=Object.getOwnPropertySymbols,go=Object.prototype.hasOwnProperty,yo=Object.prototype.propertyIsEnumerable,Fn=(e,t,n)=>t in e?gi(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Oi=(e,t)=>{for(var n in t||(t={}))go.call(t,n)&&Fn(e,n,t[n]);if(jt)for(var n of jt(t))yo.call(t,n)&&Fn(e,n,t[n]);return e},Si=(e,t)=>yi(e,bi(t)),wi=(e,t)=>{var n={};for(var r in e)go.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&jt)for(var r of jt(e))t.indexOf(r)<0&&yo.call(e,r)&&(n[r]=e[r]);return n};function ki(e,t,n={}){const r=n,{eventFilter:o}=r,a=wi(r,["eventFilter"]),{eventFilter:i,pause:c,resume:l,isActive:u}=li(o);return{stop:_i(e,t,Si(Oi({},a),{eventFilter:i})),pause:c,resume:l,isActive:u}}function Ei(e){var t;const n=ot(e);return(t=n==null?void 0:n.$el)!=null?t:n}const Ct=vo?window:void 0;function Un(...e){let t,n,r,o;if(ii(e[0])||Array.isArray(e[0])?([n,r,o]=e,t=Ct):[t,n,r,o]=e,!t)return ci;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const a=[],i=()=>{a.forEach(s=>s()),a.length=0},c=(s,p,d,m)=>(s.addEventListener(p,d,m),()=>s.removeEventListener(p,d,m)),l=Ie(()=>[Ei(t),ot(o)],([s,p])=>{i(),s&&a.push(...n.flatMap(d=>r.map(m=>c(s,d,m,p))))},{immediate:!0,flush:"post"}),u=()=>{l(),i()};return _o(u),u}function Pi(e,t=!1){const n=Y(),r=()=>n.value=Boolean(e());return r(),pi(r,t),n}function Ii(e,t={}){const{window:n=Ct}=t,r=Pi(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const a=Y(!1),i=()=>{!o||("removeEventListener"in o?o.removeEventListener("change",c):o.removeListener(c))},c=()=>{!r.value||(i(),o=n.matchMedia(fi(e).value),a.value=o.matches,"addEventListener"in o?o.addEventListener("change",c):o.addListener(c))};return Ea(c),_o(()=>i()),a}const en=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},tn="__vueuse_ssr_handlers__";en[tn]=en[tn]||{};const ji=en[tn];function Ci(e,t){return ji[e]||t}function Di(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}var xi=Object.defineProperty,Vn=Object.getOwnPropertySymbols,Li=Object.prototype.hasOwnProperty,Ni=Object.prototype.propertyIsEnumerable,Wn=(e,t,n)=>t in e?xi(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Kn=(e,t)=>{for(var n in t||(t={}))Li.call(t,n)&&Wn(e,n,t[n]);if(Vn)for(var n of Vn(t))Ni.call(t,n)&&Wn(e,n,t[n]);return e};const Ai={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},zn="vueuse-storage";function Ti(e,t,n,r={}){var o;const{flush:a="pre",deep:i=!0,listenToStorageChanges:c=!0,writeDefaults:l=!0,mergeDefaults:u=!1,shallow:s,window:p=Ct,eventFilter:d,onError:m=j=>{console.error(j)}}=r,g=(s?ka:Y)(t);if(!n)try{n=Ci("getDefaultStorage",()=>{var j;return(j=Ct)==null?void 0:j.localStorage})()}catch(j){m(j)}if(!n)return g;const v=ot(t),h=Di(v),_=(o=r.serializer)!=null?o:Ai[h],{pause:w,resume:O}=ki(g,()=>y(g.value),{flush:a,deep:i,eventFilter:d});return p&&c&&(Un(p,"storage",F),Un(p,zn,N)),F(),g;function y(j){try{if(j==null)n.removeItem(e);else{const C=_.write(j),$=n.getItem(e);$!==C&&(n.setItem(e,C),p&&p.dispatchEvent(new CustomEvent(zn,{detail:{key:e,oldValue:$,newValue:C,storageArea:n}})))}}catch(C){m(C)}}function k(j){const C=j?j.newValue:n.getItem(e);if(C==null)return l&&v!==null&&n.setItem(e,_.write(v)),v;if(!j&&u){const $=_.read(C);return ai(u)?u($,v):h==="object"&&!Array.isArray($)?Kn(Kn({},v),$):$}else return typeof C!="string"?C:_.read(C)}function N(j){F(j.detail)}function F(j){if(!(j&&j.storageArea!==n)){if(j&&j.key==null){g.value=v;return}if(!(j&&j.key!==e)){w();try{g.value=k(j)}catch(C){m(C)}finally{j?yn(O):O()}}}}}function Ri(e){return Ii("(prefers-color-scheme: dark)",e)}var Jn;(function(e){e.UP="UP",e.RIGHT="RIGHT",e.DOWN="DOWN",e.LEFT="LEFT",e.NONE="NONE"})(Jn||(Jn={}));var Mi=Object.defineProperty,Qn=Object.getOwnPropertySymbols,$i=Object.prototype.hasOwnProperty,Hi=Object.prototype.propertyIsEnumerable,Gn=(e,t,n)=>t in e?Mi(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Bi=(e,t)=>{for(var n in t||(t={}))$i.call(t,n)&&Gn(e,n,t[n]);if(Qn)for(var n of Qn(t))Hi.call(t,n)&&Gn(e,n,t[n]);return e};const qi={easeInSine:[.12,0,.39,0],easeOutSine:[.61,1,.88,1],easeInOutSine:[.37,0,.63,1],easeInQuad:[.11,0,.5,0],easeOutQuad:[.5,1,.89,1],easeInOutQuad:[.45,0,.55,1],easeInCubic:[.32,0,.67,0],easeOutCubic:[.33,1,.68,1],easeInOutCubic:[.65,0,.35,1],easeInQuart:[.5,0,.75,0],easeOutQuart:[.25,1,.5,1],easeInOutQuart:[.76,0,.24,1],easeInQuint:[.64,0,.78,0],easeOutQuint:[.22,1,.36,1],easeInOutQuint:[.83,0,.17,1],easeInExpo:[.7,0,.84,0],easeOutExpo:[.16,1,.3,1],easeInOutExpo:[.87,0,.13,1],easeInCirc:[.55,0,1,.45],easeOutCirc:[0,.55,.45,1],easeInOutCirc:[.85,0,.15,1],easeInBack:[.36,0,.66,-.56],easeOutBack:[.34,1.56,.64,1],easeInOutBack:[.68,-.6,.32,1.6]};Bi({linear:ui},qi);const X=()=>Qa(),bo=Symbol(""),Sn=()=>{const e=gn(bo);if(!e)throw new Error("useDarkMode() is called without provider.");return e},Fi=()=>{const e=X(),t=Ri(),n=Ti("vuepress-color-scheme",e.value.colorMode),r=I({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});lo(bo,r),Ui(r)},Ui=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};_e(()=>{Ie(e,t,{immediate:!0})}),so(()=>t())},Oo=(...e)=>{const n=qe().resolve(...e),r=n.matched[n.matched.length-1];if(!(r!=null&&r.redirect))return n;const{redirect:o}=r,a=Pa(o)?o(n):o,i=$t(a)?{path:a}:a;return Oo({hash:n.hash,query:n.query,params:n.params,...i})},wn=e=>{const t=Oo(encodeURI(e));return{text:t.meta.title||e,link:t.name==="404"?e:t.fullPath}};let Kt=null,Qe=null;const Vi={wait:()=>Kt,pending:()=>{Kt=new Promise(e=>Qe=e)},resolve:()=>{Qe==null||Qe(),Kt=null,Qe=null}},So=()=>Vi,wo=Symbol("sidebarItems"),kn=()=>{const e=gn(wo);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},Wi=()=>{const e=X(),t=de(),n=I(()=>Ki(t.value,e.value));lo(wo,n)},Ki=(e,t)=>{var o,a,i,c;const n=(a=(o=e.sidebar)!=null?o:t.sidebar)!=null?a:"auto",r=(c=(i=e.sidebarDepth)!=null?i:t.sidebarDepth)!=null?c:2;return e.home||n===!1?[]:n==="auto"?Ji(r):Ht(n)?ko(n,r):uo(n)?Qi(n,r):[]},zi=(e,t)=>({text:e.title,link:e.link,children:En(e.children,t)}),En=(e,t)=>t>0?e.map(n=>zi(n,t-1)):[],Ji=e=>{const t=$e();return[{text:t.value.title,children:En(t.value.headers,e)}]},ko=(e,t)=>{const n=je(),r=$e(),o=a=>{var c;let i;if($t(a)?i=wn(a):i=a,i.children)return{...i,children:i.children.map(l=>o(l))};if(i.link===n.path){const l=((c=r.value.headers[0])==null?void 0:c.level)===1?r.value.headers[0].children:r.value.headers;return{...i,children:En(l,t)}}return i};return e.map(a=>o(a))},Qi=(e,t)=>{var a;const n=je(),r=Ia(e,n.path),o=(a=e[r])!=null?a:[];return ko(o,t)},Gi={},Zi={class:"theme-default-content"};function Yi(e,t){const n=he("Content");return S(),P("div",Zi,[H(n)])}const Xi=q(Gi,[["render",Yi],["__file","HomeContent.vue"]]),ec={key:0,class:"features"},tc=R({__name:"HomeFeatures",setup(e){const t=de(),n=I(()=>Ht(t.value.features)?t.value.features:[]);return(r,o)=>b(n).length?(S(),P("div",ec,[(S(!0),P(ie,null,Oe(b(n),a=>(S(),P("div",{key:a.title,class:"feature"},[T("h2",null,z(a.title),1),T("p",null,z(a.details),1)]))),128))])):V("v-if",!0)}}),nc=q(tc,[["__file","HomeFeatures.vue"]]),rc=["innerHTML"],oc=["textContent"],ac=R({__name:"HomeFooter",setup(e){const t=de(),n=I(()=>t.value.footer),r=I(()=>t.value.footerHtml);return(o,a)=>b(n)?(S(),P(ie,{key:0},[V(" eslint-disable-next-line vue/no-v-html "),b(r)?(S(),P("div",{key:0,class:"footer",innerHTML:b(n)},null,8,rc)):(S(),P("div",{key:1,class:"footer",textContent:z(b(n))},null,8,oc))],64)):V("v-if",!0)}}),ic=q(ac,[["__file","HomeFooter.vue"]]),cc=["href","rel","target","aria-label"],sc=R({inheritAttrs:!1}),lc=R({...sc,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=je(),r=oo(),{item:o}=bn(t),a=I(()=>Bt(o.value.link)),i=I(()=>ja(o.value.link)||Ca(o.value.link)),c=I(()=>{if(!i.value){if(o.value.target)return o.value.target;if(a.value)return"_blank"}}),l=I(()=>c.value==="_blank"),u=I(()=>!a.value&&!i.value&&!l.value),s=I(()=>{if(!i.value){if(o.value.rel)return o.value.rel;if(l.value)return"noopener noreferrer"}}),p=I(()=>o.value.ariaLabel||o.value.text),d=I(()=>{const v=Object.keys(r.value.locales);return v.length?!v.some(h=>h===o.value.link):o.value.link!=="/"}),m=I(()=>d.value?n.path.startsWith(o.value.link):!1),g=I(()=>u.value?o.value.activeMatch?new RegExp(o.value.activeMatch).test(n.path):m.value:!1);return(v,h)=>{const _=he("RouterLink"),w=he("AutoLinkExternalIcon");return b(u)?(S(),J(_,Nn({key:0,class:{"router-link-active":b(g)},to:b(o).link,"aria-label":b(p)},v.$attrs),{default:Z(()=>[M(v.$slots,"before"),Be(" "+z(b(o).text)+" ",1),M(v.$slots,"after")]),_:3},16,["class","to","aria-label"])):(S(),P("a",Nn({key:1,class:"external-link",href:b(o).link,rel:b(s),target:b(c),"aria-label":b(p)},v.$attrs),[M(v.$slots,"before"),Be(" "+z(b(o).text)+" ",1),b(l)?(S(),J(w,{key:0})):V("v-if",!0),M(v.$slots,"after")],16,cc))}}}),me=q(lc,[["__file","AutoLink.vue"]]),uc={class:"hero"},fc={key:0,id:"main-title"},pc={key:1,class:"description"},dc={key:2,class:"actions"},mc=R({__name:"HomeHero",setup(e){const t=de(),n=hn(),r=Sn(),o=I(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),a=I(()=>t.value.heroAlt||c.value||"hero"),i=I(()=>t.value.heroHeight||280),c=I(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello"),l=I(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site"),u=I(()=>Ht(t.value.actions)?t.value.actions.map(({text:p,link:d,type:m="primary"})=>({text:p,link:d,type:m})):[]),s=()=>{if(!o.value)return null;const p=U("img",{src:ao(o.value),alt:a.value,height:i.value});return t.value.heroImageDark===void 0?p:U(io,()=>p)};return(p,d)=>(S(),P("header",uc,[H(s),b(c)?(S(),P("h1",fc,z(b(c)),1)):V("v-if",!0),b(l)?(S(),P("p",pc,z(b(l)),1)):V("v-if",!0),b(u).length?(S(),P("p",dc,[(S(!0),P(ie,null,Oe(b(u),m=>(S(),J(me,{key:m.text,class:ce(["action-button",[m.type]]),item:m},null,8,["class","item"]))),128))])):V("v-if",!0)]))}}),vc=q(mc,[["__file","HomeHero.vue"]]),hc={class:"home"},_c=R({__name:"Home",setup(e){return(t,n)=>(S(),P("main",hc,[H(vc),H(nc),H(Xi),H(ic)]))}}),gc=q(_c,[["__file","Home.vue"]]),yc=R({__name:"NavbarBrand",setup(e){const t=lt(),n=hn(),r=X(),o=Sn(),a=I(()=>r.value.home||t.value),i=I(()=>n.value.title),c=I(()=>o.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),l=()=>{if(!c.value)return null;const u=U("img",{class:"logo",src:ao(c.value),alt:i.value});return r.value.logoDark===void 0?u:U(io,()=>u)};return(u,s)=>{const p=he("RouterLink");return S(),J(p,{to:b(a)},{default:Z(()=>[H(l),b(i)?(S(),P("span",{key:0,class:ce(["site-name",{"can-hide":b(c)}])},z(b(i)),3)):V("v-if",!0)]),_:1},8,["to"])}}}),bc=q(yc,[["__file","NavbarBrand.vue"]]),Oc=R({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,o)=>(S(),J(_n,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:Z(()=>[M(r.$slots,"default")]),_:3}))}}),Eo=q(Oc,[["__file","DropdownTransition.vue"]]),Sc=["aria-label"],wc={class:"title"},kc=T("span",{class:"arrow down"},null,-1),Ec=["aria-label"],Pc={class:"title"},Ic={class:"navbar-dropdown"},jc={class:"navbar-dropdown-subtitle"},Cc={key:1},Dc={class:"navbar-dropdown-subitem-wrapper"},xc=R({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=bn(t),r=I(()=>n.value.ariaLabel||n.value.text),o=Y(!1),a=je();Ie(()=>a.path,()=>{o.value=!1});const i=l=>{l.detail===0?o.value=!o.value:o.value=!1},c=(l,u)=>u[u.length-1]===l;return(l,u)=>(S(),P("div",{class:ce(["navbar-dropdown-wrapper",{open:o.value}])},[T("button",{class:"navbar-dropdown-title",type:"button","aria-label":b(r),onClick:i},[T("span",wc,z(b(n).text),1),kc],8,Sc),T("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":b(r),onClick:u[0]||(u[0]=s=>o.value=!o.value)},[T("span",Pc,z(b(n).text),1),T("span",{class:ce(["arrow",o.value?"down":"right"])},null,2)],8,Ec),H(Eo,null,{default:Z(()=>[Pt(T("ul",Ic,[(S(!0),P(ie,null,Oe(b(n).children,s=>(S(),P("li",{key:s.text,class:"navbar-dropdown-item"},[s.children?(S(),P(ie,{key:0},[T("h4",jc,[s.link?(S(),J(me,{key:0,item:s,onFocusout:p=>c(s,b(n).children)&&s.children.length===0&&(o.value=!1)},null,8,["item","onFocusout"])):(S(),P("span",Cc,z(s.text),1))]),T("ul",Dc,[(S(!0),P(ie,null,Oe(s.children,p=>(S(),P("li",{key:p.link,class:"navbar-dropdown-subitem"},[H(me,{item:p,onFocusout:d=>c(p,s.children)&&c(s,b(n).children)&&(o.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(S(),J(me,{key:1,item:s,onFocusout:p=>c(s,b(n).children)&&(o.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[It,o.value]])]),_:1})],2))}}),Lc=q(xc,[["__file","NavbarDropdown.vue"]]),Zn=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),Nc=(e,t)=>{if(t.hash===e)return!0;const n=Zn(t.path),r=Zn(e);return n===r},Po=(e,t)=>e.link&&Nc(e.link,t)?!0:e.children?e.children.some(n=>Po(n,t)):!1,Io=e=>!Bt(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,Ac={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},Tc=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Io(e);return n!==null?Ac[n]:null},Rc=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const a=Tc({docsRepo:e,editLinkPattern:o});return a?a.replace(/:repo/,Bt(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,Da(`${xa(n)}/${r}`)):null},Mc={key:0,class:"navbar-items"},$c=R({__name:"NavbarItems",setup(e){const t=()=>{const s=qe(),p=lt(),d=hn(),m=X();return I(()=>{var O,y,k;const g=Object.keys(d.value.locales);if(g.length<2)return[];const v=s.currentRoute.value.path,h=s.currentRoute.value.fullPath,_=s.currentRoute.value.hash;return[{text:(O=m.value.selectLanguageText)!=null?O:"unknown language",ariaLabel:(k=(y=m.value.selectLanguageAriaLabel)!=null?y:m.value.selectLanguageText)!=null?k:"unknown language",children:g.map(N=>{var oe,Q,we,se,Ce,ge;const F=(Q=(oe=d.value.locales)==null?void 0:oe[N])!=null?Q:{},j=(se=(we=m.value.locales)==null?void 0:we[N])!=null?se:{},C=`${F.lang}`,$=(Ce=j.selectLanguageName)!=null?Ce:C;let K;if(C===d.value.lang)K=h;else{const De=v.replace(p.value,N);s.getRoutes().some(ft=>ft.path===De)?K=`${De}${_}`:K=(ge=j.home)!=null?ge:N}return{text:$,link:K}})}]})},n=()=>{const s=X(),p=I(()=>s.value.repo),d=I(()=>p.value?Io(p.value):null),m=I(()=>p.value&&!Bt(p.value)?`https://github.com/${p.value}`:p.value),g=I(()=>m.value?s.value.repoLabel?s.value.repoLabel:d.value===null?"Source":d.value:null);return I(()=>!m.value||!g.value?[]:[{text:g.value,link:m.value}])},r=s=>$t(s)?wn(s):s.children?{...s,children:s.children.map(r)}:s,o=()=>{const s=X();return I(()=>(s.value.navbar||[]).map(r))},a=Y(!1),i=o(),c=t(),l=n(),u=I(()=>[...i.value,...c.value,...l.value]);return _e(()=>{const p=()=>{window.innerWidth<719?a.value=!0:a.value=!1};p(),window.addEventListener("resize",p,!1),window.addEventListener("orientationchange",p,!1)}),(s,p)=>b(u).length?(S(),P("nav",Mc,[(S(!0),P(ie,null,Oe(b(u),d=>(S(),P("div",{key:d.text,class:"navbar-item"},[d.children?(S(),J(Lc,{key:0,item:d,class:ce(a.value?"mobile":"")},null,8,["item","class"])):(S(),J(me,{key:1,item:d},null,8,["item"]))]))),128))])):V("v-if",!0)}}),jo=q($c,[["__file","NavbarItems.vue"]]),Hc=["title"],Bc={class:"icon",focusable:"false",viewBox:"0 0 32 32"},qc=La('',9),Fc=[qc],Uc={class:"icon",focusable:"false",viewBox:"0 0 32 32"},Vc=T("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Wc=[Vc],Kc=R({__name:"ToggleColorModeButton",setup(e){const t=X(),n=Sn(),r=()=>{n.value=!n.value};return(o,a)=>(S(),P("button",{class:"toggle-color-mode-button",title:b(t).toggleColorMode,onClick:r},[Pt((S(),P("svg",Bc,Fc,512)),[[It,!b(n)]]),Pt((S(),P("svg",Uc,Wc,512)),[[It,b(n)]])],8,Hc))}}),zc=q(Kc,[["__file","ToggleColorModeButton.vue"]]),Jc=["title"],Qc=T("div",{class:"icon","aria-hidden":"true"},[T("span"),T("span"),T("span")],-1),Gc=[Qc],Zc=R({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=X();return(n,r)=>(S(),P("div",{class:"toggle-sidebar-button",title:b(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=o=>n.$emit("toggle"))},Gc,8,Jc))}}),Yc=q(Zc,[["__file","ToggleSidebarButton.vue"]]),Xc=R({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=X(),n=Y(null),r=Y(null),o=Y(0),a=I(()=>o.value?{maxWidth:o.value+"px"}:{});_e(()=>{const l=i(n.value,"paddingLeft")+i(n.value,"paddingRight"),u=()=>{var s;window.innerWidth<719?o.value=0:o.value=n.value.offsetWidth-l-(((s=r.value)==null?void 0:s.offsetWidth)||0)};u(),window.addEventListener("resize",u,!1),window.addEventListener("orientationchange",u,!1)});function i(c,l){var p,d,m;const u=(m=(d=(p=c==null?void 0:c.ownerDocument)==null?void 0:p.defaultView)==null?void 0:d.getComputedStyle(c,null))==null?void 0:m[l],s=Number.parseInt(u,10);return Number.isNaN(s)?0:s}return(c,l)=>{const u=he("NavbarSearch");return S(),P("header",{ref_key:"navbar",ref:n,class:"navbar"},[H(Yc,{onToggle:l[0]||(l[0]=s=>c.$emit("toggle-sidebar"))}),T("span",{ref_key:"navbarBrand",ref:r},[H(bc)],512),T("div",{class:"navbar-items-wrapper",style:co(b(a))},[M(c.$slots,"before"),H(jo,{class:"can-hide"}),M(c.$slots,"after"),b(t).colorModeSwitch?(S(),J(zc,{key:0})):V("v-if",!0),H(u)],4)],512)}}}),es=q(Xc,[["__file","Navbar.vue"]]),ts={class:"page-meta"},ns={key:0,class:"meta-item edit-link"},rs={key:1,class:"meta-item last-updated"},os={class:"meta-item-label"},as={class:"meta-item-info"},is={key:2,class:"meta-item contributors"},cs={class:"meta-item-label"},ss={class:"meta-item-info"},ls=["title"],us=R({__name:"PageMeta",setup(e){const t=()=>{const l=X(),u=$e(),s=de();return I(()=>{var w,O,y;if(!((O=(w=s.value.editLink)!=null?w:l.value.editLink)!=null?O:!0))return null;const{repo:d,docsRepo:m=d,docsBranch:g="main",docsDir:v="",editLinkText:h}=l.value;if(!m)return null;const _=Rc({docsRepo:m,docsBranch:g,docsDir:v,filePathRelative:u.value.filePathRelative,editLinkPattern:(y=s.value.editLinkPattern)!=null?y:l.value.editLinkPattern});return _?{text:h!=null?h:"Edit this page",link:_}:null})},n=()=>{const l=X(),u=$e(),s=de();return I(()=>{var m,g,v,h;return!((g=(m=s.value.lastUpdated)!=null?m:l.value.lastUpdated)!=null?g:!0)||!((v=u.value.git)!=null&&v.updatedTime)?null:new Date((h=u.value.git)==null?void 0:h.updatedTime).toLocaleString()})},r=()=>{const l=X(),u=$e(),s=de();return I(()=>{var d,m,g,v;return((m=(d=s.value.contributors)!=null?d:l.value.contributors)!=null?m:!0)&&(v=(g=u.value.git)==null?void 0:g.contributors)!=null?v:null})},o=X(),a=t(),i=n(),c=r();return(l,u)=>{const s=he("ClientOnly");return S(),P("footer",ts,[b(a)?(S(),P("div",ns,[H(me,{class:"meta-item-label",item:b(a)},null,8,["item"])])):V("v-if",!0),b(i)?(S(),P("div",rs,[T("span",os,z(b(o).lastUpdatedText)+": ",1),H(s,null,{default:Z(()=>[T("span",as,z(b(i)),1)]),_:1})])):V("v-if",!0),b(c)&&b(c).length?(S(),P("div",is,[T("span",cs,z(b(o).contributorsText)+": ",1),T("span",ss,[(S(!0),P(ie,null,Oe(b(c),(p,d)=>(S(),P(ie,{key:d},[T("span",{class:"contributor",title:`email: ${p.email}`},z(p.name),9,ls),d!==b(c).length-1?(S(),P(ie,{key:0},[Be(", ")],64)):V("v-if",!0)],64))),128))])])):V("v-if",!0)])}}}),fs=q(us,[["__file","PageMeta.vue"]]),ps={key:0,class:"page-nav"},ds={class:"inner"},ms={key:0,class:"prev"},vs={key:1,class:"next"},hs=R({__name:"PageNav",setup(e){const t=l=>l===!1?null:$t(l)?wn(l):uo(l)?l:!1,n=(l,u,s)=>{const p=l.findIndex(d=>d.link===u);if(p!==-1){const d=l[p+s];return d!=null&&d.link?d:null}for(const d of l)if(d.children){const m=n(d.children,u,s);if(m)return m}return null},r=de(),o=kn(),a=je(),i=I(()=>{const l=t(r.value.prev);return l!==!1?l:n(o.value,a.path,-1)}),c=I(()=>{const l=t(r.value.next);return l!==!1?l:n(o.value,a.path,1)});return(l,u)=>b(i)||b(c)?(S(),P("nav",ps,[T("p",ds,[b(i)?(S(),P("span",ms,[H(me,{item:b(i)},null,8,["item"])])):V("v-if",!0),b(c)?(S(),P("span",vs,[H(me,{item:b(c)},null,8,["item"])])):V("v-if",!0)])])):V("v-if",!0)}}),_s=q(hs,[["__file","PageNav.vue"]]),gs={class:"page"},ys={class:"theme-default-content"},bs=R({__name:"Page",setup(e){return(t,n)=>{const r=he("Content");return S(),P("main",gs,[M(t.$slots,"top"),T("div",ys,[M(t.$slots,"content-top"),H(r),M(t.$slots,"content-bottom")]),H(fs),H(_s),M(t.$slots,"bottom")])}}}),Os=q(bs,[["__file","Page.vue"]]),Ss=["onKeydown"],ws={class:"sidebar-item-children"},ks=R({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=bn(t),o=je(),a=qe(),i=I(()=>Po(n.value,o)),c=I(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:i.value,collapsible:n.value.collapsible})),l=I(()=>n.value.collapsible?i.value:!0),[u,s]=di(l.value),p=m=>{n.value.collapsible&&(m.preventDefault(),s())},d=a.afterEach(m=>{yn(()=>{u.value=l.value})});return Na(()=>{d()}),(m,g)=>{var h;const v=he("SidebarItem",!0);return S(),P("li",null,[b(n).link?(S(),J(me,{key:0,class:ce(b(c)),item:b(n)},null,8,["class","item"])):(S(),P("p",{key:1,tabindex:"0",class:ce(b(c)),onClick:p,onKeydown:Aa(p,["enter"])},[Be(z(b(n).text)+" ",1),b(n).collapsible?(S(),P("span",{key:0,class:ce(["arrow",b(u)?"down":"right"])},null,2)):V("v-if",!0)],42,Ss)),(h=b(n).children)!=null&&h.length?(S(),J(Eo,{key:2},{default:Z(()=>[Pt(T("ul",ws,[(S(!0),P(ie,null,Oe(b(n).children,_=>(S(),J(v,{key:`${b(r)}${_.text}${_.link}`,item:_,depth:b(r)+1},null,8,["item","depth"]))),128))],512),[[It,b(u)]])]),_:1})):V("v-if",!0)])}}}),Es=q(ks,[["__file","SidebarItem.vue"]]),Ps={key:0,class:"sidebar-items"},Is=R({__name:"SidebarItems",setup(e){const t=je(),n=kn();return _e(()=>{Ie(()=>t.hash,r=>{const o=document.querySelector(".sidebar");if(!o)return;const a=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!a)return;const{top:i,height:c}=o.getBoundingClientRect(),{top:l,height:u}=a.getBoundingClientRect();li+c&&a.scrollIntoView(!1)})}),(r,o)=>b(n).length?(S(),P("ul",Ps,[(S(!0),P(ie,null,Oe(b(n),a=>(S(),J(Es,{key:`${a.text}${a.link}`,item:a},null,8,["item"]))),128))])):V("v-if",!0)}}),js=q(Is,[["__file","SidebarItems.vue"]]),Cs={class:"sidebar"},Ds=R({__name:"Sidebar",setup(e){return(t,n)=>(S(),P("aside",Cs,[H(jo),M(t.$slots,"top"),H(js),M(t.$slots,"bottom")]))}}),xs=q(Ds,[["__file","Sidebar.vue"]]),Ls=R({__name:"Layout",setup(e){const t=$e(),n=de(),r=X(),o=I(()=>n.value.navbar!==!1&&r.value.navbar!==!1),a=kn(),i=Y(!1),c=h=>{i.value=typeof h=="boolean"?h:!i.value},l={x:0,y:0},u=h=>{l.x=h.changedTouches[0].clientX,l.y=h.changedTouches[0].clientY},s=h=>{const _=h.changedTouches[0].clientX-l.x,w=h.changedTouches[0].clientY-l.y;Math.abs(_)>Math.abs(w)&&Math.abs(_)>40&&(_>0&&l.x<=80?c(!0):c(!1))},p=I(()=>[{"no-navbar":!o.value,"no-sidebar":!a.value.length,"sidebar-open":i.value},n.value.pageClass]);let d;_e(()=>{d=qe().afterEach(()=>{c(!1)})}),so(()=>{d()});const m=So(),g=m.resolve,v=m.pending;return(h,_)=>(S(),P("div",{class:ce(["theme-container",b(p)]),onTouchstart:u,onTouchend:s},[M(h.$slots,"navbar",{},()=>[b(o)?(S(),J(es,{key:0,onToggleSidebar:c},{before:Z(()=>[M(h.$slots,"navbar-before")]),after:Z(()=>[M(h.$slots,"navbar-after")]),_:3})):V("v-if",!0)]),T("div",{class:"sidebar-mask",onClick:_[0]||(_[0]=w=>c(!1))}),M(h.$slots,"sidebar",{},()=>[H(xs,null,{top:Z(()=>[M(h.$slots,"sidebar-top")]),bottom:Z(()=>[M(h.$slots,"sidebar-bottom")]),_:3})]),M(h.$slots,"page",{},()=>[b(n).home?(S(),J(gc,{key:0})):(S(),J(_n,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:b(g),onBeforeLeave:b(v)},{default:Z(()=>[(S(),J(Os,{key:b(t).path},{top:Z(()=>[M(h.$slots,"page-top")]),"content-top":Z(()=>[M(h.$slots,"page-content-top")]),"content-bottom":Z(()=>[M(h.$slots,"page-content-bottom")]),bottom:Z(()=>[M(h.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),Ns=q(Ls,[["__file","Layout.vue"]]),As={class:"theme-container"},Ts={class:"page"},Rs={class:"theme-default-content"},Ms=T("h1",null,"404",-1),$s=R({__name:"NotFound",setup(e){var c,l,u;const t=lt(),n=X(),r=(c=n.value.notFound)!=null?c:["Not Found"],o=()=>r[Math.floor(Math.random()*r.length)],a=(l=n.value.home)!=null?l:t.value,i=(u=n.value.backToHome)!=null?u:"Back to home";return(s,p)=>{const d=he("RouterLink");return S(),P("div",As,[T("main",Ts,[T("div",Rs,[Ms,T("blockquote",null,z(o()),1),H(d,{to:b(a)},{default:Z(()=>[Be(z(b(i)),1)]),_:1},8,["to"])])])])}}}),Hs=q($s,[["__file","NotFound.vue"]]);const Bs=Fe({enhance({app:e,router:t}){e.component("Badge",Xa),e.component("CodeGroup",ei),e.component("CodeGroupItem",oi),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?U(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?U(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await So().wait(),n(...r))},setup(){Fi(),Wi()},layouts:{Layout:Ns,NotFound:Hs}}),qs={enhance:({app:e})=>{e.component("CodeTabs",An(()=>Ln(()=>import("./CodeTabs.14b1e322.js"),["assets/CodeTabs.14b1e322.js","assets/framework.fe9a73df.js"]))),e.component("Prop",An(()=>Ln(()=>import("./Prop.6195109b.js"),["assets/Prop.6195109b.js","assets/app.09d0ccf5.js","assets/framework.fe9a73df.js"])))}};/*! @docsearch/js 3.3.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */function Yn(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function A(e){for(var t=1;t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Dt(e,t){return function(n){if(Array.isArray(n))return n}(e)||function(n,r){var o=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(o!=null){var a,i,c=[],l=!0,u=!1;try{for(o=o.call(n);!(l=(a=o.next()).done)&&(c.push(a.value),!r||c.length!==r);l=!0);}catch(s){u=!0,i=s}finally{try{l||o.return==null||o.return()}finally{if(u)throw i}}return c}}(e,t)||Co(e,t)||function(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function xt(e){return function(t){if(Array.isArray(t))return rn(t)}(e)||function(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}(e)||Co(e)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function Co(e,t){if(e){if(typeof e=="string")return rn(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?rn(e,t):void 0}}function rn(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n3)for(n=[n],a=3;a0?nt(m.type,m.props,m.key,null,m.__v):m)!=null){if(m.__=n,m.__b=n.__b+1,(d=_[s])===null||d&&m.key==d.key&&m.type===d.type)_[s]=void 0;else for(p=0;p3)for(n=[n],a=3;a=n.__.length&&n.__.push({}),n.__[e]}function qo(e){return He=1,Fo(Vo,e)}function Fo(e,t,n){var r=ut(Ue++,2);return r.t=e,r.__c||(r.__=[n?n(t):Vo(void 0,t),function(o){var a=r.t(r.__[0],o);r.__[0]!==a&&(r.__=[a,r.__[1]],r.__c.setState({}))}],r.__c=ee),r.__}function Uo(e,t){var n=ut(Ue++,3);!E.__s&&jn(n.__H,t)&&(n.__=e,n.__H=t,ee.__H.__h.push(n))}function lr(e,t){var n=ut(Ue++,4);!E.__s&&jn(n.__H,t)&&(n.__=e,n.__H=t,ee.__h.push(n))}function zt(e,t){var n=ut(Ue++,7);return jn(n.__H,t)&&(n.__=e(),n.__H=t,n.__h=e),n.__}function Js(){an.forEach(function(e){if(e.__P)try{e.__H.__h.forEach(St),e.__H.__h.forEach(cn),e.__H.__h=[]}catch(t){e.__H.__h=[],E.__e(t,e.__v)}}),an=[]}E.__b=function(e){ee=null,or&&or(e)},E.__r=function(e){ar&&ar(e),Ue=0;var t=(ee=e.__c).__H;t&&(t.__h.forEach(St),t.__h.forEach(cn),t.__h=[])},E.diffed=function(e){ir&&ir(e);var t=e.__c;t&&t.__H&&t.__H.__h.length&&(an.push(t)!==1&&rr===E.requestAnimationFrame||((rr=E.requestAnimationFrame)||function(n){var r,o=function(){clearTimeout(a),ur&&cancelAnimationFrame(r),setTimeout(n)},a=setTimeout(o,100);ur&&(r=requestAnimationFrame(o))})(Js)),ee=void 0},E.__c=function(e,t){t.some(function(n){try{n.__h.forEach(St),n.__h=n.__h.filter(function(r){return!r.__||cn(r)})}catch(r){t.some(function(o){o.__h&&(o.__h=[])}),t=[],E.__e(r,n.__v)}}),cr&&cr(e,t)},E.unmount=function(e){sr&&sr(e);var t=e.__c;if(t&&t.__H)try{t.__H.__.forEach(St)}catch(n){E.__e(n,t.__v)}};var ur=typeof requestAnimationFrame=="function";function St(e){var t=ee;typeof e.__c=="function"&&e.__c(),ee=t}function cn(e){var t=ee;e.__c=e.__(),ee=t}function jn(e,t){return!e||e.length!==t.length||t.some(function(n,r){return n!==e[r]})}function Vo(e,t){return typeof t=="function"?t(e):t}function Wo(e,t){for(var n in t)e[n]=t[n];return e}function sn(e,t){for(var n in e)if(n!=="__source"&&!(n in t))return!0;for(var r in t)if(r!=="__source"&&e[r]!==t[r])return!0;return!1}function ln(e){this.props=e}(ln.prototype=new ue).isPureReactComponent=!0,ln.prototype.shouldComponentUpdate=function(e,t){return sn(this.props,e)||sn(this.state,t)};var fr=E.__b;E.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),fr&&fr(e)};var Qs=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,pr=function(e,t){return e==null?null:ve(ve(e).map(t))},Gs={map:pr,forEach:pr,count:function(e){return e?ve(e).length:0},only:function(e){var t=ve(e);if(t.length!==1)throw"Children.only";return t[0]},toArray:ve},Zs=E.__e;function wt(){this.__u=0,this.t=null,this.__b=null}function Ko(e){var t=e.__.__c;return t&&t.__e&&t.__e(e)}function Xe(){this.u=null,this.o=null}E.__e=function(e,t,n){if(e.then){for(var r,o=t;o=o.__;)if((r=o.__c)&&r.__c)return t.__e==null&&(t.__e=n.__e,t.__k=n.__k),r.__c(e,t)}Zs(e,t,n)},(wt.prototype=new ue).__c=function(e,t){var n=t.__c,r=this;r.t==null&&(r.t=[]),r.t.push(n);var o=Ko(r.__v),a=!1,i=function(){a||(a=!0,n.componentWillUnmount=n.__c,o?o(c):c())};n.__c=n.componentWillUnmount,n.componentWillUnmount=function(){i(),n.__c&&n.__c()};var c=function(){if(!--r.__u){if(r.state.__e){var u=r.state.__e;r.__v.__k[0]=function p(d,m,g){return d&&(d.__v=null,d.__k=d.__k&&d.__k.map(function(v){return p(v,m,g)}),d.__c&&d.__c.__P===m&&(d.__e&&g.insertBefore(d.__e,d.__d),d.__c.__e=!0,d.__c.__P=g)),d}(u,u.__c.__P,u.__c.__O)}var s;for(r.setState({__e:r.__b=null});s=r.t.pop();)s.forceUpdate()}},l=t.__h===!0;r.__u++||l||r.setState({__e:r.__b=r.__v.__k[0]}),e.then(i,i)},wt.prototype.componentWillUnmount=function(){this.t=[]},wt.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var n=document.createElement("div"),r=this.__v.__k[0].__c;this.__v.__k[0]=function a(i,c,l){return i&&(i.__c&&i.__c.__H&&(i.__c.__H.__.forEach(function(u){typeof u.__c=="function"&&u.__c()}),i.__c.__H=null),(i=Wo({},i)).__c!=null&&(i.__c.__P===l&&(i.__c.__P=c),i.__c=null),i.__k=i.__k&&i.__k.map(function(u){return a(u,c,l)})),i}(this.__b,n,r.__O=r.__P)}this.__b=null}var o=t.__e&&le(Se,null,e.fallback);return o&&(o.__h=null),[le(Se,null,t.__e?null:e.children),o]};var dr=function(e,t,n){if(++n[1]===n[0]&&e.o.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.o.size))for(n=e.u;n;){for(;n.length>3;)n.pop()();if(n[1]>>1,1),t.i.removeChild(r)}}),ct(le(Ys,{context:t.context},e.__v),t.l)):t.l&&t.componentWillUnmount()}function zo(e,t){return le(Xs,{__v:e,i:t})}(Xe.prototype=new ue).__e=function(e){var t=this,n=Ko(t.__v),r=t.o.get(e);return r[0]++,function(o){var a=function(){t.props.revealOrder?(r.push(o),dr(t,e,r)):o()};n?n(a):a()}},Xe.prototype.render=function(e){this.u=null,this.o=new Map;var t=ve(e.children);e.revealOrder&&e.revealOrder[0]==="b"&&t.reverse();for(var n=t.length;n--;)this.o.set(t[n],this.u=[1,0,this.u]);return e.children},Xe.prototype.componentDidUpdate=Xe.prototype.componentDidMount=function(){var e=this;this.o.forEach(function(t,n){dr(e,n,t)})};var Jo=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.element")||60103,el=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|fill|flood|font|glyph(?!R)|horiz|marker(?!H|W|U)|overline|paint|stop|strikethrough|stroke|text(?!L)|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,tl=function(e){return(typeof Symbol<"u"&&at(Symbol())=="symbol"?/fil|che|rad/i:/fil|che|ra/i).test(e)};function Qo(e,t,n){return t.__k==null&&(t.textContent=""),ct(e,t),typeof n=="function"&&n(),e?e.__c:null}ue.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(e){Object.defineProperty(ue.prototype,e,{configurable:!0,get:function(){return this["UNSAFE_"+e]},set:function(t){Object.defineProperty(this,e,{configurable:!0,writable:!0,value:t})}})});var mr=E.event;function nl(){}function rl(){return this.cancelBubble}function ol(){return this.defaultPrevented}E.event=function(e){return mr&&(e=mr(e)),e.persist=nl,e.isPropagationStopped=rl,e.isDefaultPrevented=ol,e.nativeEvent=e};var Go,vr={configurable:!0,get:function(){return this.class}},hr=E.vnode;E.vnode=function(e){var t=e.type,n=e.props,r=n;if(typeof t=="string"){for(var o in r={},n){var a=n[o];o==="value"&&"defaultValue"in n&&a==null||(o==="defaultValue"&&"value"in n&&n.value==null?o="value":o==="download"&&a===!0?a="":/ondoubleclick/i.test(o)?o="ondblclick":/^onchange(textarea|input)/i.test(o+t)&&!tl(n.type)?o="oninput":/^on(Ani|Tra|Tou|BeforeInp)/.test(o)?o=o.toLowerCase():el.test(o)?o=o.replace(/[A-Z0-9]/,"-$&").toLowerCase():a===null&&(a=void 0),r[o]=a)}t=="select"&&r.multiple&&Array.isArray(r.value)&&(r.value=ve(n.children).forEach(function(i){i.props.selected=r.value.indexOf(i.props.value)!=-1})),t=="select"&&r.defaultValue!=null&&(r.value=ve(n.children).forEach(function(i){i.props.selected=r.multiple?r.defaultValue.indexOf(i.props.value)!=-1:r.defaultValue==i.props.value})),e.props=r}t&&n.class!=n.className&&(vr.enumerable="className"in n,n.className!=null&&(r.class=n.className),Object.defineProperty(r,"className",vr)),e.$$typeof=Jo,hr&&hr(e)};var _r=E.__r;E.__r=function(e){_r&&_r(e),Go=e.__c};var al={ReactCurrentDispatcher:{current:{readContext:function(e){return Go.__n[e.__c].props.value}}}};(typeof performance>"u"?"undefined":at(performance))=="object"&&typeof performance.now=="function"&&performance.now.bind(performance);function gr(e){return!!e&&e.$$typeof===Jo}var f={useState:qo,useReducer:Fo,useEffect:Uo,useLayoutEffect:lr,useRef:function(e){return He=5,zt(function(){return{current:e}},[])},useImperativeHandle:function(e,t,n){He=6,lr(function(){typeof e=="function"?e(t()):e&&(e.current=t())},n==null?n:n.concat(e))},useMemo:zt,useCallback:function(e,t){return He=8,zt(function(){return e},t)},useContext:function(e){var t=ee.context[e.__c],n=ut(Ue++,9);return n.__c=e,t?(n.__==null&&(n.__=!0,t.sub(ee)),t.props.value):e.__},useDebugValue:function(e,t){E.useDebugValue&&E.useDebugValue(t?t(e):e)},version:"16.8.0",Children:Gs,render:Qo,hydrate:function(e,t,n){return Bo(e,t),typeof n=="function"&&n(),e?e.__c:null},unmountComponentAtNode:function(e){return!!e.__k&&(ct(null,e),!0)},createPortal:zo,createElement:le,createContext:function(e,t){var n={__c:t="__cC"+xo++,__:e,Consumer:function(r,o){return r.children(o)},Provider:function(r){var o,a;return this.getChildContext||(o=[],(a={})[t]=this,this.getChildContext=function(){return a},this.shouldComponentUpdate=function(i){this.props.value!==i.value&&o.some(on)},this.sub=function(i){o.push(i);var c=i.componentWillUnmount;i.componentWillUnmount=function(){o.splice(o.indexOf(i),1),c&&c.call(i)}}),r.children}};return n.Provider.__=n.Consumer.contextType=n},createFactory:function(e){return le.bind(null,e)},cloneElement:function(e){return gr(e)?zs.apply(null,arguments):e},createRef:function(){return{current:null}},Fragment:Se,isValidElement:gr,findDOMNode:function(e){return e&&(e.base||e.nodeType===1&&e)||null},Component:ue,PureComponent:ln,memo:function(e,t){function n(o){var a=this.props.ref,i=a==o.ref;return!i&&a&&(a.call?a(null):a.current=null),t?!t(this.props,o)||!i:sn(this.props,o)}function r(o){return this.shouldComponentUpdate=n,le(e,o)}return r.displayName="Memo("+(e.displayName||e.name)+")",r.prototype.isReactComponent=!0,r.__f=!0,r},forwardRef:function(e){function t(n,r){var o=Wo({},n);return delete o.ref,e(o,(r=n.ref||r)&&(at(r)!="object"||"current"in r)?r:null)}return t.$$typeof=Qs,t.render=t,t.prototype.isReactComponent=t.__f=!0,t.displayName="ForwardRef("+(e.displayName||e.name)+")",t},unstable_batchedUpdates:function(e,t){return e(t)},StrictMode:Se,Suspense:wt,SuspenseList:Xe,lazy:function(e){var t,n,r;function o(a){if(t||(t=e()).then(function(i){n=i.default||i},function(i){r=i}),r)throw r;if(!n)throw t;return le(n,a)}return o.displayName="Lazy",o.__f=!0,o},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:al};function il(){return f.createElement("svg",{width:"15",height:"15",className:"DocSearch-Control-Key-Icon"},f.createElement("path",{d:"M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953",strokeWidth:"1.2",stroke:"currentColor",fill:"none",strokeLinecap:"square"}))}function Zo(){return f.createElement("svg",{width:"20",height:"20",className:"DocSearch-Search-Icon",viewBox:"0 0 20 20"},f.createElement("path",{d:"M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}var cl=["translations"];function un(){return un=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var ul=f.forwardRef(function(e,t){var n=e.translations,r=n===void 0?{}:n,o=ll(e,cl),a=r.buttonText,i=a===void 0?"Search":a,c=r.buttonAriaLabel,l=c===void 0?"Search":c,u=sl(qo(null),2),s=u[0],p=u[1];return Uo(function(){typeof navigator<"u"&&(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)?p("⌘"):p("Ctrl"))},[]),f.createElement("button",un({type:"button",className:"DocSearch DocSearch-Button","aria-label":l},o,{ref:t}),f.createElement("span",{className:"DocSearch-Button-Container"},f.createElement(Zo,null),f.createElement("span",{className:"DocSearch-Button-Placeholder"},i)),f.createElement("span",{className:"DocSearch-Button-Keys"},s!==null&&f.createElement(f.Fragment,null,f.createElement("kbd",{className:"DocSearch-Button-Key"},s==="Ctrl"?f.createElement(il,null):s),f.createElement("kbd",{className:"DocSearch-Button-Key"},"K"))))});function st(e){return e.reduce(function(t,n){return t.concat(n)},[])}var fl=0;function fn(e){return e.collections.length===0?0:e.collections.reduce(function(t,n){return t+n.items.length},0)}var Yo=function(){},pl=[{segment:"autocomplete-core",version:"1.7.2"}];function kt(e,t){var n=t;return{then:function(r,o){return kt(e.then(vt(r,n,e),vt(o,n,e)),n)},catch:function(r){return kt(e.catch(vt(r,n,e)),n)},finally:function(r){return r&&n.onCancelList.push(r),kt(e.finally(vt(r&&function(){return n.onCancelList=[],r()},n,e)),n)},cancel:function(){n.isCanceled=!0;var r=n.onCancelList;n.onCancelList=[],r.forEach(function(o){o()})},isCanceled:function(){return n.isCanceled===!0}}}function br(e){return kt(e,{isCanceled:!1,onCancelList:[]})}function vt(e,t,n){return e?function(r){return t.isCanceled?r:e(r)}:n}function Or(e,t,n,r){if(!n)return null;if(e<0&&(t===null||r!==null&&t===0))return n+e;var o=(t===null?-1:t)+e;return o<=-1||o>=n?r===null?null:0:o}function Sr(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function dl(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ml(e,t){var n=[];return Promise.resolve(e(t)).then(function(r){return Promise.all(r.filter(function(o){return Boolean(o)}).map(function(o){if(o.sourceId,n.includes(o.sourceId))throw new Error("[Autocomplete] The `sourceId` ".concat(JSON.stringify(o.sourceId)," is not unique."));n.push(o.sourceId);var a=function(i){for(var c=1;ce.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var xr,Gt,bt,Ze=null,Lr=(xr=-1,Gt=-1,bt=void 0,function(e){var t=++xr;return Promise.resolve(e).then(function(n){return bt&&t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var Nl=["props","refresh","store"],Al=["inputElement","formElement","panelElement"],Tl=["inputElement"],Rl=["inputElement","maxLength"],Ml=["item","source"];function Ar(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function G(e){for(var t=1;t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Hl(e){var t=e.props,n=e.refresh,r=e.store,o=Ye(e,Nl);return{getEnvironmentProps:function(a){var i=a.inputElement,c=a.formElement,l=a.panelElement;function u(s){!r.getState().isOpen&&r.pendingRequests.isEmpty()||s.target===i||[c,l].some(function(p){return d=p,m=s.target,d===m||d.contains(m);var d,m})===!1&&(r.dispatch("blur",null),t.debug||r.pendingRequests.cancelAll())}return G({onTouchStart:u,onMouseDown:u,onTouchMove:function(s){r.getState().isOpen!==!1&&i===t.environment.document.activeElement&&s.target!==i&&i.blur()}},Ye(a,Al))},getRootProps:function(a){return G({role:"combobox","aria-expanded":r.getState().isOpen,"aria-haspopup":"listbox","aria-owns":r.getState().isOpen?"".concat(t.id,"-list"):void 0,"aria-labelledby":"".concat(t.id,"-label")},a)},getFormProps:function(a){return a.inputElement,G({action:"",noValidate:!0,role:"search",onSubmit:function(i){var c;i.preventDefault(),t.onSubmit(G({event:i,refresh:n,state:r.getState()},o)),r.dispatch("submit",null),(c=a.inputElement)===null||c===void 0||c.blur()},onReset:function(i){var c;i.preventDefault(),t.onReset(G({event:i,refresh:n,state:r.getState()},o)),r.dispatch("reset",null),(c=a.inputElement)===null||c===void 0||c.focus()}},Ye(a,Tl))},getLabelProps:function(a){return G({htmlFor:"".concat(t.id,"-input"),id:"".concat(t.id,"-label")},a)},getInputProps:function(a){var i;function c(v){(t.openOnFocus||Boolean(r.getState().query))&&Te(G({event:v,props:t,query:r.getState().completion||r.getState().query,refresh:n,store:r},o)),r.dispatch("focus",null)}var l=a||{},u=(l.inputElement,l.maxLength),s=u===void 0?512:u,p=Ye(l,Rl),d=Re(r.getState()),m=function(v){return Boolean(v&&v.match(vl))}(((i=t.environment.navigator)===null||i===void 0?void 0:i.userAgent)||""),g=d!=null&&d.itemUrl&&!m?"go":"search";return G({"aria-autocomplete":"both","aria-activedescendant":r.getState().isOpen&&r.getState().activeItemId!==null?"".concat(t.id,"-item-").concat(r.getState().activeItemId):void 0,"aria-controls":r.getState().isOpen?"".concat(t.id,"-list"):void 0,"aria-labelledby":"".concat(t.id,"-label"),value:r.getState().completion||r.getState().query,id:"".concat(t.id,"-input"),autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",enterKeyHint:g,spellCheck:"false",autoFocus:t.autoFocus,placeholder:t.placeholder,maxLength:s,type:"search",onChange:function(v){Te(G({event:v,props:t,query:v.currentTarget.value.slice(0,s),refresh:n,store:r},o))},onKeyDown:function(v){(function(h){var _=h.event,w=h.props,O=h.refresh,y=h.store,k=Ll(h,Dl);if(_.key==="ArrowUp"||_.key==="ArrowDown"){var N=function(){var Q=w.environment.document.getElementById("".concat(w.id,"-item-").concat(y.getState().activeItemId));Q&&(Q.scrollIntoViewIfNeeded?Q.scrollIntoViewIfNeeded(!1):Q.scrollIntoView(!1))},F=function(){var Q=Re(y.getState());if(y.getState().activeItemId!==null&&Q){var we=Q.item,se=Q.itemInputValue,Ce=Q.itemUrl,ge=Q.source;ge.onActive(Ee({event:_,item:we,itemInputValue:se,itemUrl:Ce,refresh:O,source:ge,state:y.getState()},k))}};_.preventDefault(),y.getState().isOpen===!1&&(w.openOnFocus||Boolean(y.getState().query))?Te(Ee({event:_,props:w,query:y.getState().query,refresh:O,store:y},k)).then(function(){y.dispatch(_.key,{nextActiveItemId:w.defaultActiveItemId}),F(),setTimeout(N,0)}):(y.dispatch(_.key,{}),F(),N())}else if(_.key==="Escape")_.preventDefault(),y.dispatch(_.key,null),y.pendingRequests.cancelAll();else if(_.key==="Tab")y.dispatch("blur",null),y.pendingRequests.cancelAll();else if(_.key==="Enter"){if(y.getState().activeItemId===null||y.getState().collections.every(function(Q){return Q.items.length===0}))return void(w.debug||y.pendingRequests.cancelAll());_.preventDefault();var j=Re(y.getState()),C=j.item,$=j.itemInputValue,K=j.itemUrl,oe=j.source;if(_.metaKey||_.ctrlKey)K!==void 0&&(oe.onSelect(Ee({event:_,item:C,itemInputValue:$,itemUrl:K,refresh:O,source:oe,state:y.getState()},k)),w.navigator.navigateNewTab({itemUrl:K,item:C,state:y.getState()}));else if(_.shiftKey)K!==void 0&&(oe.onSelect(Ee({event:_,item:C,itemInputValue:$,itemUrl:K,refresh:O,source:oe,state:y.getState()},k)),w.navigator.navigateNewWindow({itemUrl:K,item:C,state:y.getState()}));else if(!_.altKey){if(K!==void 0)return oe.onSelect(Ee({event:_,item:C,itemInputValue:$,itemUrl:K,refresh:O,source:oe,state:y.getState()},k)),void w.navigator.navigate({itemUrl:K,item:C,state:y.getState()});Te(Ee({event:_,nextState:{isOpen:!1},props:w,query:$,refresh:O,store:y},k)).then(function(){oe.onSelect(Ee({event:_,item:C,itemInputValue:$,itemUrl:K,refresh:O,source:oe,state:y.getState()},k))})}}})(G({event:v,props:t,refresh:n,store:r},o))},onFocus:c,onBlur:Yo,onClick:function(v){a.inputElement!==t.environment.document.activeElement||r.getState().isOpen||c(v)}},p)},getPanelProps:function(a){return G({onMouseDown:function(i){i.preventDefault()},onMouseLeave:function(){r.dispatch("mouseleave",null)}},a)},getListProps:function(a){return G({role:"listbox","aria-labelledby":"".concat(t.id,"-label"),id:"".concat(t.id,"-list")},a)},getItemProps:function(a){var i=a.item,c=a.source,l=Ye(a,Ml);return G({id:"".concat(t.id,"-item-").concat(i.__autocomplete_id),role:"option","aria-selected":r.getState().activeItemId===i.__autocomplete_id,onMouseMove:function(u){if(i.__autocomplete_id!==r.getState().activeItemId){r.dispatch("mousemove",i.__autocomplete_id);var s=Re(r.getState());if(r.getState().activeItemId!==null&&s){var p=s.item,d=s.itemInputValue,m=s.itemUrl,g=s.source;g.onActive(G({event:u,item:p,itemInputValue:d,itemUrl:m,refresh:n,source:g,state:r.getState()},o))}}},onMouseDown:function(u){u.preventDefault()},onClick:function(u){var s=c.getItemInputValue({item:i,state:r.getState()}),p=c.getItemUrl({item:i,state:r.getState()});(p?Promise.resolve():Te(G({event:u,nextState:{isOpen:!1},props:t,query:s,refresh:n,store:r},o))).then(function(){c.onSelect(G({event:u,item:i,itemInputValue:s,itemUrl:p,refresh:n,source:c,state:r.getState()},o))})}},l)}}}function Tr(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function Bl(e){for(var t=1;t0},reshape:function(d){return d.sources}},c),{},{id:(u=c.id)!==null&&u!==void 0?u:"autocomplete-".concat(fl++),plugins:p,initialState:Ne({activeItemId:null,query:"",completion:null,collections:[],isOpen:!1,status:"idle",context:{}},c.initialState),onStateChange:function(d){var m;(m=c.onStateChange)===null||m===void 0||m.call(c,d),p.forEach(function(g){var v;return(v=g.onStateChange)===null||v===void 0?void 0:v.call(g,d)})},onSubmit:function(d){var m;(m=c.onSubmit)===null||m===void 0||m.call(c,d),p.forEach(function(g){var v;return(v=g.onSubmit)===null||v===void 0?void 0:v.call(g,d)})},onReset:function(d){var m;(m=c.onReset)===null||m===void 0||m.call(c,d),p.forEach(function(g){var v;return(v=g.onReset)===null||v===void 0?void 0:v.call(g,d)})},getSources:function(d){return Promise.all([].concat(bl(p.map(function(m){return m.getSources})),[c.getSources]).filter(Boolean).map(function(m){return ml(m,d)})).then(function(m){return st(m)}).then(function(m){return m.map(function(g){return Ne(Ne({},g),{},{onSelect:function(v){g.onSelect(v),l.forEach(function(h){var _;return(_=h.onSelect)===null||_===void 0?void 0:_.call(h,v)})},onActive:function(v){g.onActive(v),l.forEach(function(h){var _;return(_=h.onActive)===null||_===void 0?void 0:_.call(h,v)})}})})})},navigator:Ne({navigate:function(d){var m=d.itemUrl;s.location.assign(m)},navigateNewTab:function(d){var m=d.itemUrl,g=s.open(m,"_blank","noopener");g==null||g.focus()},navigateNewWindow:function(d){var m=d.itemUrl;s.open(m,"_blank","noopener")}},c.navigator)})}(e,t),r=gl(Ul,n,function(c){var l=c.prevState,u=c.state;n.onStateChange(Pe({prevState:l,state:u,refresh:i},o))}),o=function(c){var l=c.store;return{setActiveItemId:function(u){l.dispatch("setActiveItemId",u)},setQuery:function(u){l.dispatch("setQuery",u)},setCollections:function(u){var s=0,p=u.map(function(d){return _t(_t({},d),{},{items:st(d.items).map(function(m){return _t(_t({},m),{},{__autocomplete_id:s++})})})});l.dispatch("setCollections",p)},setIsOpen:function(u){l.dispatch("setIsOpen",u)},setStatus:function(u){l.dispatch("setStatus",u)},setContext:function(u){l.dispatch("setContext",u)}}}({store:r}),a=Hl(Pe({props:n,refresh:i,store:r},o));function i(){return Te(Pe({event:new Event("input"),nextState:{isOpen:r.getState().isOpen},props:n,query:r.getState().query,refresh:i,store:r},o))}return n.plugins.forEach(function(c){var l;return(l=c.subscribe)===null||l===void 0?void 0:l.call(c,Pe(Pe({},o),{},{refresh:i,onSelect:function(u){t.push({onSelect:u})},onActive:function(u){t.push({onActive:u})}}))}),function(c){var l,u,s=c.metadata,p=c.environment;if(!((l=p.navigator)===null||l===void 0||(u=l.userAgent)===null||u===void 0)&&u.includes("Algolia Crawler")){var d=p.document.createElement("meta"),m=p.document.querySelector("head");d.name="algolia:metadata",setTimeout(function(){d.content=JSON.stringify(s),m.appendChild(d)},0)}}({metadata:ql({plugins:n.plugins,options:e}),environment:n.environment}),Pe(Pe({refresh:i},a),o)}function Kl(e){var t=e.translations,n=(t===void 0?{}:t).searchByText,r=n===void 0?"Search by":n;return f.createElement("a",{href:"https://www.algolia.com/ref/docsearch/?utm_source=".concat(window.location.hostname,"&utm_medium=referral&utm_content=powered_by&utm_campaign=docsearch"),target:"_blank",rel:"noopener noreferrer"},f.createElement("span",{className:"DocSearch-Label"},r),f.createElement("svg",{width:"77",height:"19","aria-label":"Algolia",role:"img",id:"Layer_1",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 2196.2 500"},f.createElement("defs",null,f.createElement("style",null,".cls-1,.cls-2{fill:#003dff;}.cls-2{fill-rule:evenodd;}")),f.createElement("path",{className:"cls-2",d:"M1070.38,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"}),f.createElement("rect",{className:"cls-1",x:"1845.88",y:"104.73",width:"62.58",height:"277.9",rx:"5.9",ry:"5.9"}),f.createElement("path",{className:"cls-2",d:"M1851.78,71.38h50.77c3.26,0,5.9-2.64,5.9-5.9V5.9c0-3.62-3.24-6.39-6.82-5.83l-50.77,7.95c-2.87,.45-4.99,2.92-4.99,5.83v51.62c0,3.26,2.64,5.9,5.9,5.9Z"}),f.createElement("path",{className:"cls-2",d:"M1764.03,275.3V5.91c0-3.63-3.24-6.39-6.82-5.83l-50.46,7.94c-2.87,.45-4.99,2.93-4.99,5.84l.17,273.22c0,12.92,0,92.7,95.97,95.49,3.33,.1,6.09-2.58,6.09-5.91v-40.78c0-2.96-2.19-5.51-5.12-5.84-34.85-4.01-34.85-47.57-34.85-54.72Z"}),f.createElement("path",{className:"cls-2",d:"M1631.95,142.72c-11.14-12.25-24.83-21.65-40.78-28.31-15.92-6.53-33.26-9.85-52.07-9.85-18.78,0-36.15,3.17-51.92,9.85-15.59,6.66-29.29,16.05-40.76,28.31-11.47,12.23-20.38,26.87-26.76,44.03-6.38,17.17-9.24,37.37-9.24,58.36,0,20.99,3.19,36.87,9.55,54.21,6.38,17.32,15.14,32.11,26.45,44.36,11.29,12.23,24.83,21.62,40.6,28.46,15.77,6.83,40.12,10.33,52.4,10.48,12.25,0,36.78-3.82,52.7-10.48,15.92-6.68,29.46-16.23,40.78-28.46,11.29-12.25,20.05-27.04,26.25-44.36,6.22-17.34,9.24-33.22,9.24-54.21,0-20.99-3.34-41.19-10.03-58.36-6.38-17.17-15.14-31.8-26.43-44.03Zm-44.43,163.75c-11.47,15.75-27.56,23.7-48.09,23.7-20.55,0-36.63-7.8-48.1-23.7-11.47-15.75-17.21-34.01-17.21-61.2,0-26.89,5.59-49.14,17.06-64.87,11.45-15.75,27.54-23.52,48.07-23.52,20.55,0,36.63,7.78,48.09,23.52,11.47,15.57,17.36,37.98,17.36,64.87,0,27.19-5.72,45.3-17.19,61.2Z"}),f.createElement("path",{className:"cls-2",d:"M894.42,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"}),f.createElement("path",{className:"cls-2",d:"M2133.97,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-14.52,22.58-22.99,49.63-22.99,78.73,0,44.89,20.13,84.92,51.59,111.1,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47,1.23,0,2.46-.03,3.68-.09,.36-.02,.71-.05,1.07-.07,.87-.05,1.75-.11,2.62-.2,.34-.03,.68-.08,1.02-.12,.91-.1,1.82-.21,2.73-.34,.21-.03,.42-.07,.63-.1,32.89-5.07,61.56-30.82,70.9-62.81v57.83c0,3.26,2.64,5.9,5.9,5.9h50.42c3.26,0,5.9-2.64,5.9-5.9V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,206.92c-12.2,10.16-27.97,13.98-44.84,15.12-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-42.24,0-77.12-35.89-77.12-79.37,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33v142.83Z"}),f.createElement("path",{className:"cls-2",d:"M1314.05,104.73h-49.33c-48.36,0-90.91,25.48-115.75,64.1-11.79,18.34-19.6,39.64-22.11,62.59-.58,5.3-.88,10.68-.88,16.14s.31,11.15,.93,16.59c4.28,38.09,23.14,71.61,50.66,94.52,2.93,2.6,6.05,4.98,9.31,7.14,12.86,8.49,28.11,13.47,44.52,13.47h0c17.99,0,34.61-5.93,48.16-15.97,16.29-11.58,28.88-28.54,34.48-47.75v50.26h-.11v11.08c0,21.84-5.71,38.27-17.34,49.36-11.61,11.08-31.04,16.63-58.25,16.63-11.12,0-28.79-.59-46.6-2.41-2.83-.29-5.46,1.5-6.27,4.22l-12.78,43.11c-1.02,3.46,1.27,7.02,4.83,7.53,21.52,3.08,42.52,4.68,54.65,4.68,48.91,0,85.16-10.75,108.89-32.21,21.48-19.41,33.15-48.89,35.2-88.52V110.63c0-3.26-2.64-5.9-5.9-5.9h-56.32Zm0,64.1s.65,139.13,0,143.36c-12.08,9.77-27.11,13.59-43.49,14.7-.16,.01-.33,.03-.49,.04-1.12,.07-2.24,.1-3.36,.1-1.32,0-2.63-.03-3.94-.1-40.41-2.11-74.52-37.26-74.52-79.38,0-10.25,1.96-20.01,5.42-28.98,11.22-29.12,38.77-49.74,71.06-49.74h49.33Z"}),f.createElement("path",{className:"cls-1",d:"M249.83,0C113.3,0,2,110.09,.03,246.16c-2,138.19,110.12,252.7,248.33,253.5,42.68,.25,83.79-10.19,120.3-30.03,3.56-1.93,4.11-6.83,1.08-9.51l-23.38-20.72c-4.75-4.21-11.51-5.4-17.36-2.92-25.48,10.84-53.17,16.38-81.71,16.03-111.68-1.37-201.91-94.29-200.13-205.96,1.76-110.26,92-199.41,202.67-199.41h202.69V407.41l-115-102.18c-3.72-3.31-9.42-2.66-12.42,1.31-18.46,24.44-48.53,39.64-81.93,37.34-46.33-3.2-83.87-40.5-87.34-86.81-4.15-55.24,39.63-101.52,94-101.52,49.18,0,89.68,37.85,93.91,85.95,.38,4.28,2.31,8.27,5.52,11.12l29.95,26.55c3.4,3.01,8.79,1.17,9.63-3.3,2.16-11.55,2.92-23.58,2.07-35.92-4.82-70.34-61.8-126.93-132.17-131.26-80.68-4.97-148.13,58.14-150.27,137.25-2.09,77.1,61.08,143.56,138.19,145.26,32.19,.71,62.03-9.41,86.14-26.95l150.26,133.2c6.44,5.71,16.61,1.14,16.61-7.47V9.48C499.66,4.25,495.42,0,490.18,0H249.83Z"})))}function Ot(e){return f.createElement("svg",{width:"15",height:"15","aria-label":e.ariaLabel,role:"img"},f.createElement("g",{fill:"none",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:"1.2"},e.children))}function zl(e){var t=e.translations,n=t===void 0?{}:t,r=n.selectText,o=r===void 0?"to select":r,a=n.selectKeyAriaLabel,i=a===void 0?"Enter key":a,c=n.navigateText,l=c===void 0?"to navigate":c,u=n.navigateUpKeyAriaLabel,s=u===void 0?"Arrow up":u,p=n.navigateDownKeyAriaLabel,d=p===void 0?"Arrow down":p,m=n.closeText,g=m===void 0?"to close":m,v=n.closeKeyAriaLabel,h=v===void 0?"Escape key":v,_=n.searchByText,w=_===void 0?"Search by":_;return f.createElement(f.Fragment,null,f.createElement("div",{className:"DocSearch-Logo"},f.createElement(Kl,{translations:{searchByText:w}})),f.createElement("ul",{className:"DocSearch-Commands"},f.createElement("li",null,f.createElement("kbd",{className:"DocSearch-Commands-Key"},f.createElement(Ot,{ariaLabel:i},f.createElement("path",{d:"M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3"}))),f.createElement("span",{className:"DocSearch-Label"},o)),f.createElement("li",null,f.createElement("kbd",{className:"DocSearch-Commands-Key"},f.createElement(Ot,{ariaLabel:d},f.createElement("path",{d:"M7.5 3.5v8M10.5 8.5l-3 3-3-3"}))),f.createElement("kbd",{className:"DocSearch-Commands-Key"},f.createElement(Ot,{ariaLabel:s},f.createElement("path",{d:"M7.5 11.5v-8M10.5 6.5l-3-3-3 3"}))),f.createElement("span",{className:"DocSearch-Label"},l)),f.createElement("li",null,f.createElement("kbd",{className:"DocSearch-Commands-Key"},f.createElement(Ot,{ariaLabel:h},f.createElement("path",{d:"M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956"}))),f.createElement("span",{className:"DocSearch-Label"},g))))}function Jl(e){var t=e.hit,n=e.children;return f.createElement("a",{href:t.url},n)}function Ql(){return f.createElement("svg",{viewBox:"0 0 38 38",stroke:"currentColor",strokeOpacity:".5"},f.createElement("g",{fill:"none",fillRule:"evenodd"},f.createElement("g",{transform:"translate(1 1)",strokeWidth:"2"},f.createElement("circle",{strokeOpacity:".3",cx:"18",cy:"18",r:"18"}),f.createElement("path",{d:"M36 18c0-9.94-8.06-18-18-18"},f.createElement("animateTransform",{attributeName:"transform",type:"rotate",from:"0 18 18",to:"360 18 18",dur:"1s",repeatCount:"indefinite"})))))}function Gl(){return f.createElement("svg",{width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("g",{stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"},f.createElement("path",{d:"M3.18 6.6a8.23 8.23 0 1112.93 9.94h0a8.23 8.23 0 01-11.63 0"}),f.createElement("path",{d:"M6.44 7.25H2.55V3.36M10.45 6v5.6M10.45 11.6L13 13"})))}function pn(){return f.createElement("svg",{width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("path",{d:"M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}function Zl(){return f.createElement("svg",{className:"DocSearch-Hit-Select-Icon",width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("g",{stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"},f.createElement("path",{d:"M18 3v4c0 2-2 4-4 4H2"}),f.createElement("path",{d:"M8 17l-6-6 6-6"})))}var Yl=function(){return f.createElement("svg",{width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("path",{d:"M17 6v12c0 .52-.2 1-1 1H4c-.7 0-1-.33-1-1V2c0-.55.42-1 1-1h8l5 5zM14 8h-3.13c-.51 0-.87-.34-.87-.87V4",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinejoin:"round"}))};function Xl(e){switch(e.type){case"lvl1":return f.createElement(Yl,null);case"content":return f.createElement(tu,null);default:return f.createElement(eu,null)}}function eu(){return f.createElement("svg",{width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("path",{d:"M13 13h4-4V8H7v5h6v4-4H7V8H3h4V3v5h6V3v5h4-4v5zm-6 0v4-4H3h4z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinecap:"round",strokeLinejoin:"round"}))}function tu(){return f.createElement("svg",{width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("path",{d:"M17 5H3h14zm0 5H3h14zm0 5H3h14z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinejoin:"round"}))}function Hr(){return f.createElement("svg",{width:"20",height:"20",viewBox:"0 0 20 20"},f.createElement("path",{d:"M10 14.2L5 17l1-5.6-4-4 5.5-.7 2.5-5 2.5 5 5.6.8-4 4 .9 5.5z",stroke:"currentColor",fill:"none",fillRule:"evenodd",strokeLinejoin:"round"}))}function nu(){return f.createElement("svg",{width:"40",height:"40",viewBox:"0 0 20 20",fill:"none",fillRule:"evenodd",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round"},f.createElement("path",{d:"M19 4.8a16 16 0 00-2-1.2m-3.3-1.2A16 16 0 001.1 4.7M16.7 8a12 12 0 00-2.8-1.4M10 6a12 12 0 00-6.7 2M12.3 14.7a4 4 0 00-4.5 0M14.5 11.4A8 8 0 0010 10M3 16L18 2M10 18h0"}))}function ru(){return f.createElement("svg",{width:"40",height:"40",viewBox:"0 0 20 20",fill:"none",fillRule:"evenodd",stroke:"currentColor",strokeLinecap:"round",strokeLinejoin:"round"},f.createElement("path",{d:"M15.5 4.8c2 3 1.7 7-1 9.7h0l4.3 4.3-4.3-4.3a7.8 7.8 0 01-9.8 1m-2.2-2.2A7.8 7.8 0 0113.2 2.4M2 18L18 2"}))}function ou(e){var t=e.translations,n=t===void 0?{}:t,r=n.titleText,o=r===void 0?"Unable to fetch results":r,a=n.helpText,i=a===void 0?"You might want to check your network connection.":a;return f.createElement("div",{className:"DocSearch-ErrorScreen"},f.createElement("div",{className:"DocSearch-Screen-Icon"},f.createElement(nu,null)),f.createElement("p",{className:"DocSearch-Title"},o),f.createElement("p",{className:"DocSearch-Help"},i))}var au=["translations"];function iu(e){return function(t){if(Array.isArray(t))return Zt(t)}(e)||function(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}(e)||function(t,n){if(!!t){if(typeof t=="string")return Zt(t,n);var r=Object.prototype.toString.call(t).slice(8,-1);if(r==="Object"&&t.constructor&&(r=t.constructor.name),r==="Map"||r==="Set")return Array.from(t);if(r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return Zt(t,n)}}(e)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function Zt(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function su(e){var t=e.translations,n=t===void 0?{}:t,r=cu(e,au),o=n.noResultsText,a=o===void 0?"No results for":o,i=n.suggestedQueryText,c=i===void 0?"Try searching for":i,l=n.reportMissingResultsText,u=l===void 0?"Believe this query should return results?":l,s=n.reportMissingResultsLinkText,p=s===void 0?"Let us know.":s,d=r.state.context.searchSuggestions;return f.createElement("div",{className:"DocSearch-NoResults"},f.createElement("div",{className:"DocSearch-Screen-Icon"},f.createElement(ru,null)),f.createElement("p",{className:"DocSearch-Title"},a,' "',f.createElement("strong",null,r.state.query),'"'),d&&d.length>0&&f.createElement("div",{className:"DocSearch-NoResults-Prefill-List"},f.createElement("p",{className:"DocSearch-Help"},c,":"),f.createElement("ul",null,d.slice(0,3).reduce(function(m,g){return[].concat(iu(m),[f.createElement("li",{key:g},f.createElement("button",{className:"DocSearch-Prefill",key:g,type:"button",onClick:function(){r.setQuery(g.toLowerCase()+" "),r.refresh(),r.inputRef.current.focus()}},g))])},[]))),r.getMissingResultsUrl&&f.createElement("p",{className:"DocSearch-Help"},"".concat(u," "),f.createElement("a",{href:r.getMissingResultsUrl({query:r.state.query}),target:"_blank",rel:"noopener noreferrer"},p)))}var lu=["hit","attribute","tagName"];function Br(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable})),n.push.apply(n,r)}return n}function qr(e){for(var t=1;t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Fr(e,t){return t.split(".").reduce(function(n,r){return n!=null&&n[r]?n[r]:null},e)}function Ae(e){var t=e.hit,n=e.attribute,r=e.tagName;return le(r===void 0?"span":r,qr(qr({},fu(e,lu)),{},{dangerouslySetInnerHTML:{__html:Fr(t,"_snippetResult.".concat(n,".value"))||Fr(t,n)}}))}function Ur(e,t){return function(n){if(Array.isArray(n))return n}(e)||function(n,r){var o=n==null?null:typeof Symbol<"u"&&n[Symbol.iterator]||n["@@iterator"];if(o!=null){var a,i,c=[],l=!0,u=!1;try{for(o=o.call(n);!(l=(a=o.next()).done)&&(c.push(a.value),!r||c.length!==r);l=!0);}catch(s){u=!0,i=s}finally{try{l||o.return==null||o.return()}finally{if(u)throw i}}return c}}(e,t)||function(n,r){if(!!n){if(typeof n=="string")return Vr(n,r);var o=Object.prototype.toString.call(n).slice(8,-1);if(o==="Object"&&n.constructor&&(o=n.constructor.name),o==="Map"||o==="Set")return Array.from(n);if(o==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o))return Vr(n,r)}}(e,t)||function(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function Vr(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n|<\/mark>)/g,mu=RegExp(ta.source);function na(e){var t,n,r,o,a,i=e;if(!i.__docsearch_parent&&!e._highlightResult)return e.hierarchy.lvl0;var c=((i.__docsearch_parent?(t=i.__docsearch_parent)===null||t===void 0||(n=t._highlightResult)===null||n===void 0||(r=n.hierarchy)===null||r===void 0?void 0:r.lvl0:(o=e._highlightResult)===null||o===void 0||(a=o.hierarchy)===null||a===void 0?void 0:a.lvl0)||{}).value;return c&&mu.test(c)?c.replace(ta,""):c}function mn(){return mn=Object.assign||function(e){for(var t=1;t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function gu(e){var t=e.translations,n=t===void 0?{}:t,r=_u(e,hu),o=n.recentSearchesTitle,a=o===void 0?"Recent":o,i=n.noRecentSearchesText,c=i===void 0?"No recent searches":i,l=n.saveRecentSearchButtonTitle,u=l===void 0?"Save this search":l,s=n.removeRecentSearchButtonTitle,p=s===void 0?"Remove this search from history":s,d=n.favoriteSearchesTitle,m=d===void 0?"Favorite":d,g=n.removeFavoriteSearchButtonTitle,v=g===void 0?"Remove this search from favorites":g;return r.state.status==="idle"&&r.hasCollections===!1?r.disableUserPersonalization?null:f.createElement("div",{className:"DocSearch-StartScreen"},f.createElement("p",{className:"DocSearch-Help"},c)):r.hasCollections===!1?null:f.createElement("div",{className:"DocSearch-Dropdown-Container"},f.createElement(dn,Tt({},r,{title:a,collection:r.state.collections[0],renderIcon:function(){return f.createElement("div",{className:"DocSearch-Hit-icon"},f.createElement(Gl,null))},renderAction:function(h){var _=h.item,w=h.runFavoriteTransition,O=h.runDeleteTransition;return f.createElement(f.Fragment,null,f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:u,type:"submit",onClick:function(y){y.preventDefault(),y.stopPropagation(),w(function(){r.favoriteSearches.add(_),r.recentSearches.remove(_),r.refresh()})}},f.createElement(Hr,null))),f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:p,type:"submit",onClick:function(y){y.preventDefault(),y.stopPropagation(),O(function(){r.recentSearches.remove(_),r.refresh()})}},f.createElement(pn,null))))}})),f.createElement(dn,Tt({},r,{title:m,collection:r.state.collections[1],renderIcon:function(){return f.createElement("div",{className:"DocSearch-Hit-icon"},f.createElement(Hr,null))},renderAction:function(h){var _=h.item,w=h.runDeleteTransition;return f.createElement("div",{className:"DocSearch-Hit-action"},f.createElement("button",{className:"DocSearch-Hit-action-button",title:v,type:"submit",onClick:function(O){O.preventDefault(),O.stopPropagation(),w(function(){r.favoriteSearches.remove(_),r.refresh()})}},f.createElement(pn,null)))}})))}var yu=["translations"];function Rt(){return Rt=Object.assign||function(e){for(var t=1;t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}var Ou=f.memo(function(e){var t=e.translations,n=t===void 0?{}:t,r=bu(e,yu);if(r.state.status==="error")return f.createElement(ou,{translations:n==null?void 0:n.errorScreen});var o=r.state.collections.some(function(a){return a.items.length>0});return r.state.query?o===!1?f.createElement(su,Rt({},r,{translations:n==null?void 0:n.noResultsScreen})):f.createElement(vu,r):f.createElement(gu,Rt({},r,{hasCollections:o,translations:n==null?void 0:n.startScreen}))},function(e,t){return t.state.status==="loading"||t.state.status==="stalled"}),Su=["translations"];function Mt(){return Mt=Object.assign||function(e){for(var t=1;t=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function ku(e){var t=e.translations,n=t===void 0?{}:t,r=wu(e,Su),o=n.resetButtonTitle,a=o===void 0?"Clear the query":o,i=n.resetButtonAriaLabel,c=i===void 0?"Clear the query":i,l=n.cancelButtonText,u=l===void 0?"Cancel":l,s=n.cancelButtonAriaLabel,p=s===void 0?"Cancel":s,d=r.getFormProps({inputElement:r.inputRef.current}).onReset;return f.useEffect(function(){r.autoFocus&&r.inputRef.current&&r.inputRef.current.focus()},[r.autoFocus,r.inputRef]),f.useEffect(function(){r.isFromSelection&&r.inputRef.current&&r.inputRef.current.select()},[r.isFromSelection,r.inputRef]),f.createElement(f.Fragment,null,f.createElement("form",{className:"DocSearch-Form",onSubmit:function(m){m.preventDefault()},onReset:d},f.createElement("label",Mt({className:"DocSearch-MagnifierLabel"},r.getLabelProps()),f.createElement(Zo,null)),f.createElement("div",{className:"DocSearch-LoadingIndicator"},f.createElement(Ql,null)),f.createElement("input",Mt({className:"DocSearch-Input",ref:r.inputRef},r.getInputProps({inputElement:r.inputRef.current,autoFocus:r.autoFocus,maxLength:64}))),f.createElement("button",{type:"reset",title:a,className:"DocSearch-Reset","aria-label":c,hidden:!r.state.query},f.createElement(pn,null))),f.createElement("button",{className:"DocSearch-Cancel",type:"reset","aria-label":p,onClick:r.onClose},u))}var Eu=["_highlightResult","_snippetResult"];function Pu(e,t){if(e==null)return{};var n,r,o=function(i,c){if(i==null)return{};var l,u,s={},p=Object.keys(i);for(u=0;u=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Iu(e){return function(){var t="__TEST_KEY__";try{return localStorage.setItem(t,""),localStorage.removeItem(t),!0}catch{return!1}}()===!1?{setItem:function(){},getItem:function(){return[]}}:{setItem:function(t){return window.localStorage.setItem(e,JSON.stringify(t))},getItem:function(){var t=window.localStorage.getItem(e);return t?JSON.parse(t):[]}}}function zr(e){var t=e.key,n=e.limit,r=n===void 0?5:n,o=Iu(t),a=o.getItem().slice(0,r);return{add:function(i){var c=i,l=(c._highlightResult,c._snippetResult,Pu(c,Eu)),u=a.findIndex(function(s){return s.objectID===l.objectID});u>-1&&a.splice(u,1),a.unshift(l),a=a.slice(0,r),o.setItem(a)},remove:function(i){a=a.filter(function(c){return c.objectID!==i.objectID}),o.setItem(a)},getAll:function(){return a}}}var ju=["facetName","facetQuery"];function Cu(e){var t,n="algoliasearch-client-js-".concat(e.key),r=function(){return t===void 0&&(t=e.localStorage||window.localStorage),t},o=function(){return JSON.parse(r().getItem(n)||"{}")};return{get:function(a,i){var c=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return Promise.resolve().then(function(){var l=JSON.stringify(a),u=o()[l];return Promise.all([u||i(),u!==void 0])}).then(function(l){var u=Dt(l,2),s=u[0],p=u[1];return Promise.all([s,p||c.miss(s)])}).then(function(l){return Dt(l,1)[0]})},set:function(a,i){return Promise.resolve().then(function(){var c=o();return c[JSON.stringify(a)]=i,r().setItem(n,JSON.stringify(c)),i})},delete:function(a){return Promise.resolve().then(function(){var i=o();delete i[JSON.stringify(a)],r().setItem(n,JSON.stringify(i))})},clear:function(){return Promise.resolve().then(function(){r().removeItem(n)})}}}function et(e){var t=xt(e.caches),n=t.shift();return n===void 0?{get:function(r,o){var a=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return o().then(function(i){return Promise.all([i,a.miss(i)])}).then(function(i){return Dt(i,1)[0]})},set:function(r,o){return Promise.resolve(o)},delete:function(r){return Promise.resolve()},clear:function(){return Promise.resolve()}}:{get:function(r,o){var a=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}};return n.get(r,o,a).catch(function(){return et({caches:t}).get(r,o,a)})},set:function(r,o){return n.set(r,o).catch(function(){return et({caches:t}).set(r,o)})},delete:function(r){return n.delete(r).catch(function(){return et({caches:t}).delete(r)})},clear:function(){return n.clear().catch(function(){return et({caches:t}).clear()})}}}function Yt(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{serializable:!0},t={};return{get:function(n,r){var o=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{miss:function(){return Promise.resolve()}},a=JSON.stringify(n);if(a in t)return Promise.resolve(e.serializable?JSON.parse(t[a]):t[a]);var i=r(),c=o&&o.miss||function(){return Promise.resolve()};return i.then(function(l){return c(l)}).then(function(){return i})},set:function(n,r){return t[JSON.stringify(n)]=e.serializable?JSON.stringify(r):r,Promise.resolve(r)},delete:function(n){return delete t[JSON.stringify(n)],Promise.resolve()},clear:function(){return t={},Promise.resolve()}}}function Du(e){for(var t=e.length-1;t>0;t--){var n=Math.floor(Math.random()*(t+1)),r=e[t];e[t]=e[n],e[n]=r}return e}function ra(e,t){return t&&Object.keys(t).forEach(function(n){e[n]=t[n](e)}),e}function qt(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r0?r:void 0,timeout:n.timeout||t,headers:n.headers||{},queryParameters:n.queryParameters||{},cacheable:n.cacheable}}var Me={Read:1,Write:2,Any:3},oa=1,xu=2,aa=3;function ia(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:oa;return A(A({},e),{},{status:t,lastUpdate:Date.now()})}function ca(e){return typeof e=="string"?{protocol:"https",url:e,accept:Me.Any}:{protocol:e.protocol||"https",url:e.url,accept:e.accept||Me.Any}}var Qr="GET",Ft="POST";function Lu(e,t){return Promise.all(t.map(function(n){return e.get(n,function(){return Promise.resolve(ia(n))})})).then(function(n){var r=n.filter(function(i){return function(c){return c.status===oa||Date.now()-c.lastUpdate>12e4}(i)}),o=n.filter(function(i){return function(c){return c.status===aa&&Date.now()-c.lastUpdate<=12e4}(i)}),a=[].concat(xt(r),xt(o));return{getTimeout:function(i,c){return(o.length===0&&i===0?1:o.length+3+i)*c},statelessHosts:a.length>0?a.map(function(i){return ca(i)}):t}})}function Gr(e,t,n,r){var o=[],a=function(d,m){if(!(d.method===Qr||d.data===void 0&&m.data===void 0)){var g=Array.isArray(d.data)?d.data:A(A({},d.data),m.data);return JSON.stringify(g)}}(n,r),i=function(d,m){var g=A(A({},d.headers),m.headers),v={};return Object.keys(g).forEach(function(h){var _=g[h];v[h.toLowerCase()]=_}),v}(e,r),c=n.method,l=n.method!==Qr?{}:A(A({},n.data),r.data),u=A(A(A({"x-algolia-agent":e.userAgent.value},e.queryParameters),l),r.queryParameters),s=0,p=function d(m,g){var v=m.pop();if(v===void 0)throw{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:Zr(o)};var h={data:a,headers:i,method:c,url:Au(v,n.path,u),connectTimeout:g(s,e.timeouts.connect),responseTimeout:g(s,r.timeout)},_=function(O){var y={request:h,response:O,host:v,triesLeft:m.length};return o.push(y),y},w={onSucess:function(O){return function(y){try{return JSON.parse(y.content)}catch(k){throw function(N,F){return{name:"DeserializationError",message:N,response:F}}(k.message,y)}}(O)},onRetry:function(O){var y=_(O);return O.isTimedOut&&s++,Promise.all([e.logger.info("Retryable failure",la(y)),e.hostsCache.set(v,ia(v,O.isTimedOut?aa:xu))]).then(function(){return d(m,g)})},onFail:function(O){throw _(O),function(y,k){var N=y.content,F=y.status,j=N;try{j=JSON.parse(N).message}catch{}return function(C,$,K){return{name:"ApiError",message:C,status:$,transporterStackTrace:K}}(j,F,k)}(O,Zr(o))}};return e.requester.send(h).then(function(O){return function(y,k){return function(N){var F=N.status;return N.isTimedOut||function(j){var C=j.isTimedOut,$=j.status;return!C&&~~$==0}(N)||~~(F/100)!=2&&~~(F/100)!=4}(y)?k.onRetry(y):~~(y.status/100)==2?k.onSucess(y):k.onFail(y)}(O,w)})};return Lu(e.hostsCache,t).then(function(d){return p(xt(d.statelessHosts).reverse(),d.getTimeout)})}function Nu(e){var t={value:"Algolia for JavaScript (".concat(e,")"),add:function(n){var r="; ".concat(n.segment).concat(n.version!==void 0?" (".concat(n.version,")"):"");return t.value.indexOf(r)===-1&&(t.value="".concat(t.value).concat(r)),t}};return t}function Au(e,t,n){var r=sa(n),o="".concat(e.protocol,"://").concat(e.url,"/").concat(t.charAt(0)==="/"?t.substr(1):t);return r.length&&(o+="?".concat(r)),o}function sa(e){return Object.keys(e).map(function(t){return qt("%s=%s",t,(n=e[t],Object.prototype.toString.call(n)==="[object Object]"||Object.prototype.toString.call(n)==="[object Array]"?JSON.stringify(e[t]):e[t]));var n}).join("&")}function Zr(e){return e.map(function(t){return la(t)})}function la(e){var t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return A(A({},e),{},{request:A(A({},e.request),{},{headers:A(A({},e.request.headers),t)})})}var Tu=function(e){var t=e.appId,n=function(a,i,c){var l={"x-algolia-api-key":c,"x-algolia-application-id":i};return{headers:function(){return a===Et.WithinHeaders?l:{}},queryParameters:function(){return a===Et.WithinQueryParameters?l:{}}}}(e.authMode!==void 0?e.authMode:Et.WithinHeaders,t,e.apiKey),r=function(a){var i=a.hostsCache,c=a.logger,l=a.requester,u=a.requestsCache,s=a.responsesCache,p=a.timeouts,d=a.userAgent,m=a.hosts,g=a.queryParameters,v={hostsCache:i,logger:c,requester:l,requestsCache:u,responsesCache:s,timeouts:p,userAgent:d,headers:a.headers,queryParameters:g,hosts:m.map(function(h){return ca(h)}),read:function(h,_){var w=Jr(_,v.timeouts.read),O=function(){return Gr(v,v.hosts.filter(function(k){return(k.accept&Me.Read)!=0}),h,w)};if((w.cacheable!==void 0?w.cacheable:h.cacheable)!==!0)return O();var y={request:h,mappedRequestOptions:w,transporter:{queryParameters:v.queryParameters,headers:v.headers}};return v.responsesCache.get(y,function(){return v.requestsCache.get(y,function(){return v.requestsCache.set(y,O()).then(function(k){return Promise.all([v.requestsCache.delete(y),k])},function(k){return Promise.all([v.requestsCache.delete(y),Promise.reject(k)])}).then(function(k){var N=Dt(k,2);return N[0],N[1]})})},{miss:function(k){return v.responsesCache.set(y,k)}})},write:function(h,_){return Gr(v,v.hosts.filter(function(w){return(w.accept&Me.Write)!=0}),h,Jr(_,v.timeouts.write))}};return v}(A(A({hosts:[{url:"".concat(t,"-dsn.algolia.net"),accept:Me.Read},{url:"".concat(t,".algolia.net"),accept:Me.Write}].concat(Du([{url:"".concat(t,"-1.algolianet.com")},{url:"".concat(t,"-2.algolianet.com")},{url:"".concat(t,"-3.algolianet.com")}]))},e),{},{headers:A(A(A({},n.headers()),{"content-type":"application/x-www-form-urlencoded"}),e.headers),queryParameters:A(A({},n.queryParameters()),e.queryParameters)})),o={transporter:r,appId:t,addAlgoliaAgent:function(a,i){r.userAgent.add({segment:a,version:i})},clearCache:function(){return Promise.all([r.requestsCache.clear(),r.responsesCache.clear()]).then(function(){})}};return ra(o,e.methods)},ua=function(e){return function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r={transporter:e.transporter,appId:e.appId,indexName:t};return ra(r,n.methods)}},Yr=function(e){return function(t,n){var r=t.map(function(o){return A(A({},o),{},{params:sa(o.params||{})})});return e.transporter.read({method:Ft,path:"1/indexes/*/queries",data:{requests:r},cacheable:!0},n)}},Xr=function(e){return function(t,n){return Promise.all(t.map(function(r){var o=r.params,a=o.facetName,i=o.facetQuery,c=Us(o,ju);return ua(e)(r.indexName,{methods:{searchForFacetValues:fa}}).searchForFacetValues(a,i,A(A({},n),c))}))}},Ru=function(e){return function(t,n,r){return e.transporter.read({method:Ft,path:qt("1/answers/%s/prediction",e.indexName),data:{query:t,queryLanguages:n},cacheable:!0},r)}},Mu=function(e){return function(t,n){return e.transporter.read({method:Ft,path:qt("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},n)}},fa=function(e){return function(t,n,r){return e.transporter.read({method:Ft,path:qt("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:n},cacheable:!0},r)}},$u=1,Hu=2,Bu=3;function pa(e,t,n){var r,o={appId:e,apiKey:t,timeouts:{connect:1,read:2,write:30},requester:{send:function(a){return new Promise(function(i){var c=new XMLHttpRequest;c.open(a.method,a.url,!0),Object.keys(a.headers).forEach(function(p){return c.setRequestHeader(p,a.headers[p])});var l,u=function(p,d){return setTimeout(function(){c.abort(),i({status:0,content:d,isTimedOut:!0})},1e3*p)},s=u(a.connectTimeout,"Connection timeout");c.onreadystatechange=function(){c.readyState>c.OPENED&&l===void 0&&(clearTimeout(s),l=u(a.responseTimeout,"Socket timeout"))},c.onerror=function(){c.status===0&&(clearTimeout(s),clearTimeout(l),i({content:c.responseText||"Network request failed",status:c.status,isTimedOut:!1}))},c.onload=function(){clearTimeout(s),clearTimeout(l),i({content:c.responseText,status:c.status,isTimedOut:!1})},c.send(a.data)})}},logger:(r=Bu,{debug:function(a,i){return $u>=r&&console.debug(a,i),Promise.resolve()},info:function(a,i){return Hu>=r&&console.info(a,i),Promise.resolve()},error:function(a,i){return console.error(a,i),Promise.resolve()}}),responsesCache:Yt(),requestsCache:Yt({serializable:!1}),hostsCache:et({caches:[Cu({key:"".concat("4.8.5","-").concat(e)}),Yt()]}),userAgent:Nu("4.8.5").add({segment:"Browser",version:"lite"}),authMode:Et.WithinQueryParameters};return Tu(A(A(A({},o),n),{},{methods:{search:Yr,searchForFacetValues:Xr,multipleQueries:Yr,multipleSearchForFacetValues:Xr,initIndex:function(a){return function(i){return ua(a)(i,{methods:{search:Mu,searchForFacetValues:fa,findAnswers:Ru}})}}}}))}pa.version="4.8.5";var qu=["footer","searchBox"];function rt(){return rt=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0||(s[l]=i[l]);return s}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Wu(e){var t=e.appId,n=e.apiKey,r=e.indexName,o=e.placeholder,a=o===void 0?"Search docs":o,i=e.searchParameters,c=e.onClose,l=c===void 0?du:c,u=e.transformItems,s=u===void 0?Kr:u,p=e.hitComponent,d=p===void 0?Jl:p,m=e.resultsFooterComponent,g=m===void 0?function(){return null}:m,v=e.navigator,h=e.initialScrollY,_=h===void 0?0:h,w=e.transformSearchClient,O=w===void 0?Kr:w,y=e.disableUserPersonalization,k=y!==void 0&&y,N=e.initialQuery,F=N===void 0?"":N,j=e.translations,C=j===void 0?{}:j,$=e.getMissingResultsUrl,K=C.footer,oe=C.searchBox,Q=Vu(C,qu),we=Uu(f.useState({query:"",collections:[],completion:null,context:{},isOpen:!1,activeItemId:null,status:"idle"}),2),se=we[0],Ce=we[1],ge=f.useRef(null),De=f.useRef(null),ft=f.useRef(null),pt=f.useRef(null),Ve=f.useRef(null),ye=f.useRef(10),Cn=f.useRef(typeof window<"u"?window.getSelection().toString().slice(0,64):"").current,ke=f.useRef(F||Cn).current,Dn=function(x,B,te){return f.useMemo(function(){var ne=pa(x,B);return ne.addAlgoliaAgent("docsearch","3.3.0"),/docsearch.js \(.*\)/.test(ne.transporter.userAgent.value)===!1&&ne.addAlgoliaAgent("docsearch-react","3.3.0"),te(ne)},[x,B,te])}(t,n,O),xe=f.useRef(zr({key:"__DOCSEARCH_FAVORITE_SEARCHES__".concat(r),limit:10})).current,We=f.useRef(zr({key:"__DOCSEARCH_RECENT_SEARCHES__".concat(r),limit:xe.getAll().length===0?7:4})).current,Ke=f.useCallback(function(x){if(!k){var B=x.type==="content"?x.__docsearch_parent:x;B&&xe.getAll().findIndex(function(te){return te.objectID===B.objectID})===-1&&We.add(B)}},[xe,We,k]),ze=f.useMemo(function(){return Wl({id:"docsearch",defaultActiveItemId:0,placeholder:a,openOnFocus:!0,initialState:{query:ke,context:{searchSuggestions:[]}},navigator:v,onStateChange:function(x){Ce(x.state)},getSources:function(x){var B=x.query,te=x.state,ne=x.setContext,be=x.setStatus;return B?Dn.search([{query:B,indexName:r,params:Xt({attributesToRetrieve:["hierarchy.lvl0","hierarchy.lvl1","hierarchy.lvl2","hierarchy.lvl3","hierarchy.lvl4","hierarchy.lvl5","hierarchy.lvl6","content","type","url"],attributesToSnippet:["hierarchy.lvl1:".concat(ye.current),"hierarchy.lvl2:".concat(ye.current),"hierarchy.lvl3:".concat(ye.current),"hierarchy.lvl4:".concat(ye.current),"hierarchy.lvl5:".concat(ye.current),"hierarchy.lvl6:".concat(ye.current),"content:".concat(ye.current)],snippetEllipsisText:"…",highlightPreTag:"",highlightPostTag:"",hitsPerPage:20},i)}]).catch(function(W){throw W.name==="RetryError"&&be("error"),W}).then(function(W){var re=W.results[0],ae=re.hits,va=re.nbHits,Ut=Wr(ae,function(Vt){return na(Vt)});return te.context.searchSuggestions.length0&&(xn(),Ve.current&&Ve.current.focus())},[ke,xn]),f.useEffect(function(){function x(){if(De.current){var B=.01*window.innerHeight;De.current.style.setProperty("--docsearch-vh","".concat(B,"px"))}}return x(),window.addEventListener("resize",x),function(){window.removeEventListener("resize",x)}},[]),f.createElement("div",rt({ref:ge},ma({"aria-expanded":!0}),{className:["DocSearch","DocSearch-Container",se.status==="stalled"&&"DocSearch-Container--Stalled",se.status==="error"&&"DocSearch-Container--Errored"].filter(Boolean).join(" "),role:"button",tabIndex:0,onMouseDown:function(x){x.target===x.currentTarget&&l()}}),f.createElement("div",{className:"DocSearch-Modal",ref:De},f.createElement("header",{className:"DocSearch-SearchBar",ref:ft},f.createElement(ku,rt({},ze,{state:se,autoFocus:ke.length===0,inputRef:Ve,isFromSelection:Boolean(ke)&&ke===Cn,translations:oe,onClose:l}))),f.createElement("div",{className:"DocSearch-Dropdown",ref:pt},f.createElement(Ou,rt({},ze,{indexName:r,state:se,hitComponent:d,resultsFooterComponent:g,disableUserPersonalization:k,recentSearches:We,favoriteSearches:xe,inputRef:Ve,translations:Q,getMissingResultsUrl:$,onItemClick:function(x){Ke(x),l()}}))),f.createElement("footer",{className:"DocSearch-Footer"},f.createElement(zl,{translations:K}))))}function vn(){return vn=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n1&&arguments[1]!==void 0?arguments[1]:window;return typeof t=="string"?n.document.querySelector(t):t}(e.container,e.environment))}const Ju=e=>e.button===1||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey,Qu=()=>{const e=qe(),t=oo();return{hitComponent:({hit:n,children:r})=>({type:"a",ref:void 0,constructor:void 0,key:void 0,props:{href:n.url,onClick:o=>{Ju(o)||(o.preventDefault(),e.push(Tn(n.url,t.value.base)))},children:r},__v:null}),navigator:{navigate:({itemUrl:n})=>{e.push(Tn(n,t.value.base))}},transformSearchClient:n=>{const r=fo(n.search,500);return{...n,search:async(...o)=>r(...o)}}}};const Gu=R({name:"Docsearch",props:{containerId:{type:String,required:!1,default:"docsearch-container"},options:{type:Object,required:!0}},setup(e){const t=lt(),n=ga(),r=Qu(),o=I(()=>{var c;return{...e.options,...(c=e.options.locales)==null?void 0:c[t.value]}}),a=[],i=()=>{var l,u;const c=(u=(l=o.value.searchParameters)==null?void 0:l.facetFilters)!=null?u:[];a.splice(0,a.length,`lang:${n.value}`,...Ht(c)?c:[c]),zu({...r,...o.value,container:`#${e.containerId}`,searchParameters:{...o.value.searchParameters,facetFilters:a}})};return _e(()=>{i(),Ie([t,o],([c,l],[u,s])=>{c!==u&&JSON.stringify(l)!==JSON.stringify(s)&&i()}),Ie(n,(c,l)=>{if(c!==l){const u=a.findIndex(s=>s===`lang:${l}`);u>-1&&a.splice(u,1,`lang:${c}`)}})}),()=>U("div",{id:e.containerId})}}),Zu={appId:"SDGLJOI8GP",apiKey:"7aac3b70e2be40bd6bb55bc603e7bf46",indexName:"coalesce"},Yu=Fe({enhance({app:e}){e.component("Docsearch",()=>U(Gu,{options:Zu}))}}),tf=[Ma,qa,Wa,Za,Bs,qs,Yu];export{tf as clientConfigs}; diff --git a/assets/coalesce-json.html.5f8b8190.js b/assets/coalesce-json.html.5f8b8190.js new file mode 100644 index 000000000..49cd79938 --- /dev/null +++ b/assets/coalesce-json.html.5f8b8190.js @@ -0,0 +1,70 @@ +import{_ as s,y as n,z as a,a5 as e}from"./framework.fe9a73df.js";const l={},o=e(`

Code Generation Configuration

In Coalesce, all configuration of the code generation is done in a JSON file. This file is typically named coalesce.json and is typically placed in the solution root.

File Resolution

When the code generation is run by invoking dotnet coalesce, Coalesce will try to find a configuration file via the following means:

  1. If an argument is specified on the command line, it will be used as the location of the file. E.g. dotnet coalesce C:/Projects/MyProject/config.json
  2. If no argument is given, Coalesce will try to use a file in the working directory named coalesce.json
  3. If no file is found in the working directory, Coalesce will crawl up the directory tree from the working directory until a file named coalesce.json is found. If such a file is never found, an error will be thrown.

Contents

A full example of a coalesce.json file, along with an explanation of each property, is as follows:

{
+    "webProject": {
+        // Required: Path to the csproj of the web project. Path is relative to location of this coalesce.json file.
+        "projectFile": "src/Coalesce.Web/Coalesce.Web.csproj",
+
+        // Optional: Framework to use when evaluating & building dependencies.
+        // Not needed if your project only specifies a single framework - only required for multi-targeting projects.
+        "framework": "netcoreapp2.0",
+
+        // Optional: Build configuration to use when evaluating & building dependencies.
+        // Defaults to "Debug".
+        "configuration": "Debug",
+
+        // Optional: Override the namespace prefix for generated C# code.
+        // Defaults to MSBuild's \`$(RootNamespace)\` for the project.
+        "rootNamespace": "MyCompany.Coalesce.Web",
+    },
+
+    "dataProject": {
+        // Required: Path to the csproj of the data project. Path is relative to location of this coalesce.json file.
+        "projectFile": "src/Coalesce.Domain/Coalesce.Domain.csproj",
+
+        // Optional: Framework to use when evaluating & building dependencies.
+        // Not needed if your project only specifies a single framework - only required for multi-targeting projects.
+        "framework": "netstandard2.0",
+
+        // Optional: Build configuration to use when evaluating & building dependencies.
+        // Defaults to "Release".
+        "configuration": "Debug",
+    },
+
+    // The name of the root generator to use. Defaults to "Knockout".
+    // Available values are "Vue" and "Knockout".
+    "rootGenerator": "Vue",
+            
+    // If set, specifies a list of whitelisted root type names that will restrict
+    // which types Coalesce will use for code generation. 
+    // Root types are those that must be annotated with [Coalesce].
+    // Useful if want to segment a single data project into multiple web projects, 
+    // or into different areas/directories within a single web project.
+    "rootTypesWhitelist": [
+        "MyDbContext", "MyCustomDto"
+    ],
+
+    "generatorConfig": {
+        // A set of objects keyed by generator name.
+        // Generator names may optionally be qualified by their full namespace.
+        // All generators are listed when running 'dotnet coalesce' with '--verbosity debug'.
+        // For example, "Views" or "IntelliTect.Coalesce.CodeGeneration.Knockout.Generators.Views".
+        "GeneratorName": {
+            // Optional: true if the generator should be disabled.
+            "disabled": true,
+            // Optional: Configures a path relative to the default output path for the generator
+            // where that generator's output should be placed instead.
+            "targetDirectory": "../DifferentFolder"
+        },
+        // Indentation for generated C# is configurable by type (API controllers, DTO classes and regular View controllers)
+        // It defaults to 4 spaces
+        "ApiController": {
+            "indentationSize": 2 
+        },
+        "ClassDto": {
+            "indentationSize": 2 
+        },
+        "ViewController" : {
+            "indentationSize": 2
+        }
+    }
+}
+

Additional CLI Options

There are a couple of extra options which are only available as CLI parameters to dotnet coalesce. These options do not affect the behavior of the code generation - only the behavior of the CLI itself.

--debug

When this flag is specified when running dotnet coalesce, Coalesce will wait up to 60 seconds for a debugger to be attached to its process before starting code generation.

-v|--verbosity <level>

Set the verbosity of the output. Options are trace, debug, information, warning, error, critical, and none.

`,14),p=[o];function t(i,c){return n(),a("div",null,p)}const D=s(l,[["render",t],["__file","coalesce-json.html.vue"]]);export{D as default}; diff --git a/assets/coalesce-json.html.8fc0bc73.js b/assets/coalesce-json.html.8fc0bc73.js new file mode 100644 index 000000000..3d748af50 --- /dev/null +++ b/assets/coalesce-json.html.8fc0bc73.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6db94f86","path":"/topics/coalesce-json.html","title":"Code Generation Configuration","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"File Resolution","slug":"file-resolution","link":"#file-resolution","children":[]},{"level":2,"title":"Contents","slug":"contents","link":"#contents","children":[]},{"level":2,"title":"Additional CLI Options","slug":"additional-cli-options","link":"#additional-cli-options","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"topics/coalesce-json.md"}');export{e as data}; diff --git a/assets/coalesce.html.0b9c0e83.js b/assets/coalesce.html.0b9c0e83.js new file mode 100644 index 000000000..6d7f7c572 --- /dev/null +++ b/assets/coalesce.html.0b9c0e83.js @@ -0,0 +1 @@ +import{_ as a,y as s,z as l,X as e,B as t,Q as n,$ as c,P as i}from"./framework.fe9a73df.js";const d={},r=e("h1",{id:"coalesce",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#coalesce","aria-hidden":"true"},"#"),t(" [Coalesce]")],-1),h=e("p",null,"Used to mark a type or member for generation by Coalesce.",-1),u=e("p",null,[t("Some types and members will be implicitly included in generation - for example, all types represented by a "),e("code",null,"DbSet"),t(" on a "),e("code",null,"DbContext"),t(" that has a "),e("code",null,"[Coalesce]"),t(" attribute will automatically be included. Properties on these types will also be generated for unless explicitly excluded, since this is by far the most common usage scenario in Coalesce.")],-1),p=e("code",null,"[Coalesce]",-1),m=e("p",null,[t("The documentation pages for types and members that require/accept this attribute will state as such. An exhaustive list of all valid targets for "),e("code",null,"[Coalesce]"),t(" will not be found on this page.")],-1);function _(f,b){const o=i("RouterLink");return s(),l("div",null,[r,h,u,e("p",null,[t("On the other hand, "),n(o,{to:"/modeling/model-components/methods.html"},{default:c(()=>[t("Methods")]),_:1}),t(" on these types will not have endpoints generated unless they are explicitly annotated with "),p,t(" to avoid accidentally exposing methods that were perhaps not intended to be exposed.")]),m])}const x=a(d,[["render",_],["__file","coalesce.html.vue"]]);export{x as default}; diff --git a/assets/coalesce.html.2758c118.js b/assets/coalesce.html.2758c118.js new file mode 100644 index 000000000..182373a82 --- /dev/null +++ b/assets/coalesce.html.2758c118.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-19f90752","path":"/modeling/model-components/attributes/coalesce.html","title":"[Coalesce]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/coalesce.md"}');export{e as data}; diff --git a/assets/controller-action.html.0ba1d19c.js b/assets/controller-action.html.0ba1d19c.js new file mode 100644 index 000000000..331e408f3 --- /dev/null +++ b/assets/controller-action.html.0ba1d19c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3903d1f2","path":"/modeling/model-components/attributes/controller-action.html","title":"[ControllerAction]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/controller-action.md"}');export{e as data}; diff --git a/assets/controller-action.html.c00be531.js b/assets/controller-action.html.c00be531.js new file mode 100644 index 000000000..e09035095 --- /dev/null +++ b/assets/controller-action.html.c00be531.js @@ -0,0 +1,28 @@ +import{_ as r,y as c,z as i,X as n,B as s,Q as e,$ as p,a5 as t,P as a}from"./framework.fe9a73df.js";const y={},d=n("h1",{id:"controlleraction",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#controlleraction","aria-hidden":"true"},"#"),s(" [ControllerAction]")],-1),C=t(`

Example Usage

public class Person
+{
+    public int PersonId { get; set; }
+    public string LastName { get; set; }
+
+    public string PictureHash { get; set; }
+
+    [Coalesce]
+    [ControllerAction(Method = HttpMethod.Get)]
+    public static long PersonCount(AppDbContext db, string lastNameStartsWith = "")
+    {
+        return db.People.Count(f => f.LastName.StartsWith(lastNameStartsWith));
+    }
+
+    [Coalesce]
+    [ControllerAction(HttpMethod.Get, VaryByProperty = nameof(PictureHash))]
+    public IFile GetPicture(AppDbContext db)
+    {
+        return new IntelliTect.Coalesce.Models.File(db.PersonPictures
+            .Where(x => x.PersonId == this.PersonId)
+            .Select(x => x.Content)
+        )
+        {
+            ContentType = "image/jpg",
+        };
+    }
+}
+

Properties

`,3),h=t("

The HTTP method to use on the generated API Controller.

Enum values are:

  • HttpMethod.Post Use the POST method.
  • HttpMethod.Get Use the GET method.
  • HttpMethod.Put Use the PUT method.
  • HttpMethod.Delete Use the DELETE method.
  • HttpMethod.Patch Use the PATCH method.
",3),u=n("code",null,"VaryByProperty",-1),m={href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag",target:"_blank",rel:"noopener noreferrer"},v=n("code",null,"If-None-Match",-1),b=n("code",null,"VaryByProperty",-1),E=n("code",null,"Cache-Control",-1),_=n("code",null,"VaryByProperty",-1);function g(P,f){const l=a("RouterLink"),o=a("Prop"),D=a("ExternalLinkIcon");return c(),i("div",null,[d,n("p",null,[s("Specifies how a "),e(l,{to:"/modeling/model-components/methods.html"},{default:p(()=>[s("custom method")]),_:1}),s(" is exposed via HTTP. Can be used to customize the HTTP method/verb for the method, as well as caching behavior.")]),C,e(o,{def:"public HttpMethod Method { get; set; } = HttpMethod.Post;",ctor:"1"}),h,e(o,{def:"public string VaryByProperty { get; set; }"}),n("p",null,[s("For HTTP GET model instance methods, if "),u,s(" is set to the name of a property on the parent model class, "),n("a",m,[s("ETag headers"),e(D)]),s(" based on the value of this property will be used to implement caching. If the client provides a matching "),v,s(" Header with the request, the method will not be invoked and HTTP Status `304 Not Modified`` will be returned.")]),n("p",null,[s("Additionally, if the "),b,s(" is set to a client-exposed "),e(l,{to:"/modeling/model-components/properties.html"},{default:p(()=>[s("property")]),_:1}),s(", the value of the property will be included in the query string when performing API calls to invoke the method. If the query string value matches the current value on the model, a long-term "),E,s(" header will be set on the response, allowing the client to avoid making future invocations to the same method while the value of the "),_,s(" remains the same.")])])}const T=r(y,[["render",g],["__file","controller-action.html.vue"]]);export{T as default}; diff --git a/assets/controller.html.5a450f80.js b/assets/controller.html.5a450f80.js new file mode 100644 index 000000000..8e62ceef4 --- /dev/null +++ b/assets/controller.html.5a450f80.js @@ -0,0 +1,8 @@ +import{_ as i,y as c,z as p,X as o,B as e,Q as n,$ as l,a5 as a,P as r}from"./framework.fe9a73df.js";const d={},u=o("h1",{id:"controller",tabindex:"-1"},[o("a",{class:"header-anchor",href:"#controller","aria-hidden":"true"},"#"),e(" [Controller]")],-1),D=o("p",null,"Allows for control over the generated MVC Controllers.",-1),h=o("p",null,"Currently only controls over the API controllers are present, but additional properties may be added in the future.",-1),m=a(`

Example Usage

[Controller(ApiRouted = false, ApiControllerSuffix = "Gen", ApiActionsProtected = true)]
+public class Person
+{
+    public int PersonId { get; set; }
+    
+    ...
+}
+

Properties

`,3),y=a("

Determines whether or not a [Route] annotation will be placed on the generated API controller. Set to false to prevent emission of the [Route] attribute.

Use cases include:

  • Defining your routes through IRouteBuilder in Startup.cs instead
  • Preventing API controllers from being exposed by default.
  • Routing to your own custom controller that inherits from the generated API controller in order to implement more granular or complex authorization logic.
",3),f=o("p",null,"If set, will determine the name of the generated API controller.",-1),v=o("p",null,[e("Takes precedence over the value of "),o("code",null,"ApiControllerSuffix"),e(".")],-1),_=o("p",null,"If set, will be appended to the default name of the API controller generated for this model.",-1),g=o("p",null,[e("Will be overridden by the value of "),o("code",null,"ApiControllerName"),e(" if it is set.")],-1),b=a('

If true, actions on the generated API controller will have an access modifier of protected instead of public.

In order to consume the generated API controller, you must inherit from the generated controller and override each desired generated action method via hiding (i.e. use public new ..., not public override ...).

Note

If you inherit from the generated API controllers and override their methods without setting ApiActionsProtected = true, all non-overridden actions from the generated controller will still be exposed as normal.

',3);function C(A,P){const t=r("RouterLink"),s=r("Prop");return c(),p("div",null,[u,D,h,o("p",null,[e("This attribute may be placed on any type from which an API controller is generated, including "),n(t,{to:"/modeling/model-types/entities.html"},{default:l(()=>[e("Entity Models")]),_:1}),e(", "),n(t,{to:"/modeling/model-types/dtos.html"},{default:l(()=>[e("Custom DTOs")]),_:1}),e(", and "),n(t,{to:"/modeling/model-types/services.html"},{default:l(()=>[e("Services")]),_:1}),e(".")]),m,n(s,{def:"public bool ApiRouted { get; set; } = true;"}),y,n(s,{def:"public string ApiControllerName { get; set; } = null;"}),f,v,n(s,{def:"public string ApiControllerSuffix { get; set; } = null;"}),_,g,n(s,{def:"public bool ApiActionsProtected { get; set; } = false;"}),b])}const I=i(d,[["render",C],["__file","controller.html.vue"]]);export{I as default}; diff --git a/assets/controller.html.b4140e47.js b/assets/controller.html.b4140e47.js new file mode 100644 index 000000000..05923f14a --- /dev/null +++ b/assets/controller.html.b4140e47.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-10bb147d","path":"/modeling/model-components/attributes/controller.html","title":"[Controller]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/controller.md"}');export{e as data}; diff --git a/assets/create-controller.html.76caf7a4.js b/assets/create-controller.html.76caf7a4.js new file mode 100644 index 000000000..bc0e54ba3 --- /dev/null +++ b/assets/create-controller.html.76caf7a4.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6b92cba8","path":"/modeling/model-components/attributes/create-controller.html","title":"[CreateController]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/create-controller.md"}');export{e as data}; diff --git a/assets/create-controller.html.de1c3529.js b/assets/create-controller.html.de1c3529.js new file mode 100644 index 000000000..b4c5adf41 --- /dev/null +++ b/assets/create-controller.html.de1c3529.js @@ -0,0 +1,8 @@ +import{_ as a,y as n,z as l,Q as e,a5 as o,P as r}from"./framework.fe9a73df.js";const p={},t=o(`

[CreateController]

By default an API and View controller are both created. This allows for suppressing the creation of either or both of these.

Example Usage

[CreateController(view: false, api: true)]
+public class Person
+{
+    public int PersonId { get; set; }
+    
+    ...
+}
+

Properties

`,5);function c(i,D){const s=r("Prop");return n(),l("div",null,[t,e(s,{def:"public bool WillCreateView { get; set; } = true",ctor:"1"}),e(s,{def:"public bool WillCreateApi { get; set; } = true",ctor:"2"})])}const y=a(p,[["render",c],["__file","create-controller.html.vue"]]);export{y as default}; diff --git a/assets/data-sources.html.6cd385d1.js b/assets/data-sources.html.6cd385d1.js new file mode 100644 index 000000000..b319d1df3 --- /dev/null +++ b/assets/data-sources.html.6cd385d1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-2960e828","path":"/modeling/model-components/data-sources.html","title":"Data Sources","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Defining Data Sources","slug":"defining-data-sources","link":"#defining-data-sources","children":[{"level":3,"title":"Dependency Injection","slug":"dependency-injection","link":"#dependency-injection","children":[]}]},{"level":2,"title":"Consuming Data Sources","slug":"consuming-data-sources","link":"#consuming-data-sources","children":[]},{"level":2,"title":"Standard Parameters","slug":"standard-parameters","link":"#standard-parameters","children":[]},{"level":2,"title":"Custom Parameters","slug":"custom-parameters","link":"#custom-parameters","children":[{"level":3,"title":"List Auto-loading","slug":"list-auto-loading","link":"#list-auto-loading","children":[]}]},{"level":2,"title":"Standard Data Source","slug":"standard-data-source","link":"#standard-data-source","children":[{"level":3,"title":"Default Loading Behavior","slug":"default-loading-behavior","link":"#default-loading-behavior","children":[]},{"level":3,"title":"Properties","slug":"properties","link":"#properties","children":[]},{"level":3,"title":"Method Overview","slug":"method-overview","link":"#method-overview","children":[]},{"level":3,"title":"Method Details","slug":"method-details","link":"#method-details","children":[]}]},{"level":2,"title":"Globally Replacing the Standard Data Source","slug":"globally-replacing-the-standard-data-source","link":"#globally-replacing-the-standard-data-source","children":[]}],"git":{"updatedTime":1680571733000},"filePathRelative":"modeling/model-components/data-sources.md"}');export{e as data}; diff --git a/assets/data-sources.html.c1d3cc86.js b/assets/data-sources.html.c1d3cc86.js new file mode 100644 index 000000000..f1dc4fcb4 --- /dev/null +++ b/assets/data-sources.html.c1d3cc86.js @@ -0,0 +1,120 @@ +import{_ as D,y as d,z as y,X as s,B as e,Q as a,$ as l,W as c,a5 as r,P as p}from"./framework.fe9a73df.js";const u={},h=s("h1",{id:"data-sources",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#data-sources","aria-hidden":"true"},"#"),e(" Data Sources")],-1),C=s("p",null,"In addition to this standard data source, Coalesce allows you to create custom data sources that provide complete control over the way data is loaded and serialized for transfer to a requesting client. These data sources are defined on a per-model basis, and you can have as many of them as you like for each model.",-1),m={class:"table-of-contents"},v=r(`

Defining Data Sources

By default, each of your models that Coalesce exposes will expose the standard data source (IntelliTect.Coalesce.StandardDataSource<T, TContext>). This data source provides all the standard functionality one would expect - paging, sorting, searching, filtering, and so on. Each of these component pieces is implemented in one or more virtual methods, making the StandardDataSource a great place to start from when implementing your own data source. To suppress this behavior of always exposing the raw StandardDataSource, create your own custom data source and annotate it with [DefaultDataSource].

To implement your own custom data source, you simply need to define a class that implements IntelliTect.Coalesce.IDataSource<T>. To expose your data source to Coalesce, either place it as a nested class of the type T that you data source serves, or annotate it with the [Coalesce] attribute. Of course, the easiest way to create a data source that doesn't require you to re-engineer a great deal of logic would be to inherit from IntelliTect.Coalesce.StandardDataSource<T, TContext>, and then override only the parts that you need.

public class Person
+{
+    [DefaultDataSource]
+    public class IncludeFamily : StandardDataSource<Person, AppDbContext>
+    {
+        public IncludeFamily(CrudContext<AppDbContext> context) : base(context) { }
+
+        public override IQueryable<Person> GetQuery(IDataSourceParameters parameters) 
+            => Db.People
+            .Where(f => User.IsInRole("Admin") || f.CreatedById == User.GetUserId())
+            .Include(f => f.Parents).ThenInclude(s => s.Parents)
+            .Include(f => f.Cousins).ThenInclude(s => s.Parents);
+    }
+}
+
+[Coalesce]
+public class NamesStartingWithA : StandardDataSource<Person, AppDbContext>
+{
+    public NamesStartingWithA(CrudContext<AppDbContext> context) : base(context) { }
+
+    public override IQueryable<Person> GetQuery(IDataSourceParameters parameters) 
+        => Db.People.Include(f => f.Siblings).Where(f => f.FirstName.StartsWith("A"));
+}
+
`,4),b=s("code",null,"IQueryable",-1),f=s("code",null,"StandardDataSource",-1),g=s("code",null,"IncludeTree GetIncludeTree(IQueryable query, IDataSourceParameters parameters)",-1),E=r('

WARNING

If you create a custom data source that has custom logic for securing your data, be aware that the default implementation of StandardDataSource (or your custom default implementation - see below) is still exposed unless you annotate one of your custom data sources with [DefaultDataSource]. Doing so will replace the default data source with the annotated class for your type T.

Dependency Injection

All data sources are instantiated using dependency injection and your application's IServiceProvider. As a result, you can add whatever constructor parameters you desire to your data sources as long as a value for them can be resolved from your application's services. The single parameter to the StandardDataSource is resolved in this way - the CrudContext<TContext> contains the common set of objects most commonly used, including the DbContext and the ClaimsPrincipal representing the current user.

Consuming Data Sources

',4),A=s("code",null,"$dataSource",-1),_=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/models.g'")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},", "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"IncludeFamily"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),w=s("code",null,"dataSource",-1),S=s("code",null,"Coalesce.DataSource",-1),F=s("code",null,"dataSources",-1),T=s("code",null,"ListViewModels.DataSources",-1),I=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"IncludeFamily"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),x=r(`

Standard Parameters

All methods on IDataSource<T> take a parameter that contains all the client-specified parameters for things paging, searching, sorting, and filtering information. Almost all virtual methods on StandardDataSource are also passed the relevant set of parameters.

Custom Parameters

On any data source that you create, you may add additional properties annotated with [Coalesce] that will then be exposed as parameters to the client. These property parameters are currently restricted to primitives (numeric types, strings) and dates (DateTime, DateTimeOffset). Property parameter primitives may be expanded to allow for more types in the future.

[Coalesce]
+public class NamesStartingWith : StandardDataSource<Person, AppDbContext>
+{
+    public NamesStartingWith(CrudContext<AppDbContext> context) : base(context) { }
+
+    [Coalesce]
+    public string StartsWith { get; set; }
+
+    public override IQueryable<Person> GetQuery(IDataSourceParameters parameters) 
+        => Db.People.Include(f => f.Siblings)
+        .Where(f => string.IsNullOrWhitespace(StartsWith) ? true : f.FirstName.StartsWith(StartsWith));
+}
+

List Auto-loading

`,6),P=s("code",null,"$useAutoLoad",-1),L=s("code",null,"$startAutoLoad",-1),k=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/models.g'"),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'"),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#4FC1FF"}},"list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#4FC1FF"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"NamesStartingWith")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$useAutoLoad"),s("span",{style:{color:"#D4D4D4"}},"(); "),s("span",{style:{color:"#6A9955"}},"// When using options API, use $startAutoLoad(this)")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Trigger a reload:")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"startsWith"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Jo"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),B=s("p",null,"The properties created on the TypeScript objects are observables so they may be bound to directly. In order to automatically reload a list when a data source parameter changes, you must explicitly subscribe to it:",-1),M=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"NamesStartingWith"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"startsWith"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#CE9178"}},'"Jo"'),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"subscribe"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"); "),s("span",{style:{color:"#6A9955"}},"// Enables automatic reloading.")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"list"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),Q=s("h2",{id:"standard-data-source",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#standard-data-source","aria-hidden":"true"},"#"),e(" Standard Data Source")],-1),q=s("p",null,[e("The standard data sources, "),s("code",null,"IntelliTect.Coalesce.StandardDataSource"),e(" and its EntityFramework-supporting sibling "),s("code",null,"IntelliTect.Coalesce.StandardDataSource"),e(", contain a significant number of properties and methods that can be utilized and/or overridden at your leisure.")],-1),G=s("h3",{id:"default-loading-behavior",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#default-loading-behavior","aria-hidden":"true"},"#"),e(" Default Loading Behavior")],-1),V=s("code",null,"StandardDataSource",-1),W=s("code",null,"StandardDataSource.GetQuery()",-1),O=s("code",null,"base.GetQuery()",-1),N=s("code",null,"DbSet",-1),R=s("code",null,'includes = "none"',-1),j=s("p",null,[e("On the server, you can suppress this behavior by placing "),s("code",null,"[Read(NoAutoInclude = true)]"),e(" on either an entire class (affecting all navigation properties of that type), or on specific navigation properties. When placed on a entity class that holds sensitive data, this can help ensure you don't accidentally leak records due to forgetting to customize the data sources of the types whose navigation properties reference your sensitive entity.")],-1),z=s("h3",{id:"properties",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#properties","aria-hidden":"true"},"#"),e(" Properties")],-1),$=s("p",null,[e("The following properties are available for use on the "),s("code",null,"StandardDataSource"),e(" any any derived instances.")],-1),U=s("p",null,"The object passed to the constructor that contains the set of objects needed by the standard data source, and those that are most likely to be used in custom implementations.",-1),Y=s("p",null,[e("An instance of the DbContext that contains a "),s("code",null,"DbSet"),e(" for the entity served by the data source.")],-1),J=s("p",null,"The user making the current request.",-1),K=s("p",null,"The max number of search terms to process when interpreting a search term word-by-word. Override by setting a value in the constructor.",-1),X=s("p",null,"The page size to use if none is specified by the client. Override by setting a value in the constructor.",-1),H=r(`

The maximum page size that will be served. By default, client-specified page sizes will be clamped to this value. Override by setting a value in the constructor.

Method Overview

The standard data source contains 19 different methods which can be overridden in your derived class to control its behavior.

These methods often call one another, so overriding one method may cause some other method to no longer be called. The hierarchy of method calls, ignoring any logic or conditions contained within, is as follows:

GetMappedItemAsync
+    GetItemAsync
+        GetQueryAsync
+            GetQuery
+        GetIncludeTree
+    TransformResults
+
+GetMappedListAsync
+    GetListAsync
+        GetQueryAsync
+            GetQuery
+        ApplyListFiltering
+            ApplyListPropertyFilters
+                ApplyListPropertyFilter
+            ApplyListSearchTerm
+        GetListTotalCountAsync
+        ApplyListSorting
+            ApplyListClientSpecifiedSorting
+            ApplyListDefaultSorting
+        ApplyListPaging
+        GetIncludeTree
+    TrimListFields
+    TransformResults
+
+GetCountAsync
+    GetQueryAsync
+        GetQuery
+    ApplyListFiltering
+        ApplyListPropertyFilters
+            ApplyListPropertyFilter
+        ApplyListSearchTerm
+    GetListTotalCountAsync
+

Method Details

All of the methods outlined above can be overridden. A description of each of the non-interface inner methods is as follows:

`,7),Z=r("

The method is the one that you will most commonly be override in order to implement custom query logic. The default implementation of GetQueryAsync simply calls GetQuery - be aware of this in cases of complex overrides/inheritance. From this method, you could:

  • Specify additional query filtering such as row-level security or soft-delete logic. Or, restrict the data source entirely for users or whole roles by returning an empty query.
  • Include additional data using EF's .Include() and .ThenInclude().
  • Add additional edges to the serialized object graph using Coalesce's .IncludedSeparately() and .ThenIncluded().
",2),ss={class:"custom-container tip"},es=s("p",{class:"custom-container-title"},"Note",-1),as=s("code",null,"GetQuery",-1),ls=s("code",null,"IQueryable.IncludeChildren()",-1),ns=s("p",null,"Called by other methods in the standard data source to determine whether or not EF Core async methods will be used to evaluate queries. This may be globally disabled when bugs like https://github.com/dotnet/SqlClient/issues/593 are present in EF Core.",-1),os=s("p",null,[e("A simple wrapper that calls "),s("code",null,"ApplyListPropertyFilters"),e(" and "),s("code",null,"ApplyListSearchTerm"),e(".")],-1),ts=s("p",null,[e("For each value in "),s("code",null,"parameters.Filter"),e(", invoke "),s("code",null,"ApplyListPropertyFilter"),e(" to apply a filter to the query.")],-1),rs=s("p",null,"Given a property and a client-provided string value, perform some filtering on that property.",-1),ps=s("ul",null,[s("li",null,"Dates with a time component will be matched exactly."),s("li",null,"Dates with no time component will match any dates that fell on that day."),s("li",null,[e("Strings will match exactly unless an asterisk is found, in which case they will be matched with "),s("code",null,"string.StartsWith"),e(".")]),s("li",null,"Enums will match by string or numeric value. Multiple comma-delimited values will create a filter that will match on any of the provided values."),s("li",null,"Numeric values will match exactly. Multiple comma-delimited values will create a filter that will match on any of the provided values.")],-1),cs=s("p",null,[e("If any client-specified sort orders are present, invokes "),s("code",null,"ApplyListClientSpecifiedSorting"),e(". Otherwise, invokes "),s("code",null,"ApplyListDefaultSorting"),e(".")],-1),is=s("p",null,[e("Applies sorting to the query based on sort orders specified by the client. If the client specified "),s("code",null,'"none"'),e(" as the sort field, no sorting will take place.")],-1),Ds=s("p",null,[e("Applies default sorting behavior to the query, including behavior defined with use of "),s("code",null,"[DefaultOrderBy]"),e(" in C# POCOs, as well as fallback sorting to "),s("code",null,'"Name"'),e(" or primary key properties.")],-1),ds=s("p",null,"Applies paging to the query based on incoming parameters. Provides the actual page and pageSize that were used as out parameters.",-1),ys=s("p",null,[e("Simple wrapper around invoking "),s("code",null,".Count()"),e(" on a query.")],-1),us=s("p",null,"Allows for transformation of a result set after the query has been evaluated. This will be called for both lists of items and for single items. This can be used for populating non-mapped properties on a model, or conditionally loading navigation properties using logic that depends upon the contents of each loaded record.",-1),hs=s("code",null,"/save",-1),Cs=s("p",null,[e("Do not use "),s("code",null,"TransformResults"),e(" to modify any database-mapped scalar properties, since such changes could be inadvertently persisted to the database.")],-1),ms=r(`

Performs trimming of the fields of the result set based on the parameters given to the data source. Can be overridden to forcibly disable this, override the behavior to always trim specific fields, or any other functionality desired.

Globally Replacing the Standard Data Source

You can, of course, create a custom base data source that all your custom implementations inherit from. But, what if you want to override the standard data source across your entire application, so that StandardDataSource<,> will never be instantiated? You can do that too!

Simply create a class that implements IEntityFrameworkDataSource<,> (the StandardDataSource<,> already does - feel free to inherit from it), then register it at application startup like so:

public class MyDataSource<T, TContext> : StandardDataSource<T, TContext>
+    where T : class
+    where TContext : DbContext
+{
+    public MyDataSource(CrudContext<TContext> context) : base(context)
+    {
+    }
+
+    ...
+}
+
public void ConfigureServices(IServiceCollection services)
+{
+    services.AddCoalesce(b =>
+    {
+        b.AddContext<AppDbContext>();
+        b.UseDefaultDataSource(typeof(MyDataSource<,>));
+    });
+}
+

Your custom data source must have the same generic type parameters - <T, TContext>. Otherwise, the Microsoft.Extensions.DependencyInjection service provider won't know how to inject it.

`,7);function vs(bs,fs){const o=p("RouterLink"),t=p("router-link"),i=p("CodeTabs"),n=p("Prop");return d(),y("div",null,[h,s("p",null,[e("In Coalesce, all data that is retrieved from your database through the generated controllers is done so by a data source. These data sources control what data gets loaded and how it gets loaded. By default, there is a single generic data source that serves all data for your models in a generic way that fits many of the most common use cases - the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[e("Standard Data Source")]),_:1}),e(".")]),C,s("nav",m,[s("ul",null,[s("li",null,[a(t,{to:"#defining-data-sources"},{default:l(()=>[e("Defining Data Sources")]),_:1}),s("ul",null,[s("li",null,[a(t,{to:"#dependency-injection"},{default:l(()=>[e("Dependency Injection")]),_:1})])])]),s("li",null,[a(t,{to:"#consuming-data-sources"},{default:l(()=>[e("Consuming Data Sources")]),_:1})]),s("li",null,[a(t,{to:"#standard-parameters"},{default:l(()=>[e("Standard Parameters")]),_:1})]),s("li",null,[a(t,{to:"#custom-parameters"},{default:l(()=>[e("Custom Parameters")]),_:1}),s("ul",null,[s("li",null,[a(t,{to:"#list-auto-loading"},{default:l(()=>[e("List Auto-loading")]),_:1})])])]),s("li",null,[a(t,{to:"#standard-data-source"},{default:l(()=>[e("Standard Data Source")]),_:1}),s("ul",null,[s("li",null,[a(t,{to:"#default-loading-behavior"},{default:l(()=>[e("Default Loading Behavior")]),_:1})]),s("li",null,[a(t,{to:"#properties"},{default:l(()=>[e("Properties")]),_:1})]),s("li",null,[a(t,{to:"#method-overview"},{default:l(()=>[e("Method Overview")]),_:1})]),s("li",null,[a(t,{to:"#method-details"},{default:l(()=>[e("Method Details")]),_:1})])])]),s("li",null,[a(t,{to:"#globally-replacing-the-standard-data-source"},{default:l(()=>[e("Globally Replacing the Standard Data Source")]),_:1})])])]),v,s("p",null,[e("The structure of the "),b,e(" built by the various methods of "),f,e(" is used to shape and trim the structure of the DTO as it is serialized and sent out to the client. One may also override method "),g,e(" to control this explicitly. See "),a(o,{to:"/concepts/include-tree.html"},{default:l(()=>[e("Include Tree")]),_:1}),e(" for more information on how this works.")]),E,a(i,null,{vue:l(()=>[s("p",null,[e("The "),a(o,{to:"/stacks/vue/layers/viewmodels.html#viewmodels"},{default:l(()=>[e("ViewModels")]),_:1}),e(" and "),a(o,{to:"/stacks/vue/layers/viewmodels.html#listviewmodels"},{default:l(()=>[e("ListViewModels")]),_:1}),e(" each have a property called "),A,e(". This property accepts an instance of a "),a(o,{to:"/stacks/vue/layers/models.html"},{default:l(()=>[e("DataSource")]),_:1}),e(" class generated in the "),a(o,{to:"/stacks/vue/layers/models.html"},{default:l(()=>[e("Model Layer")]),_:1}),e(".")]),_]),knockout:l(()=>[s("p",null,[e("The "),a(o,{to:"/stacks/ko/client/view-model.html"},{default:l(()=>[e("TypeScript ViewModels")]),_:1}),e(" and "),a(o,{to:"/stacks/ko/client/list-view-model.html"},{default:l(()=>[e("TypeScript ListViewModels")]),_:1}),e(" each have a property called "),w,e(". These properties accept an instance of a "),S,e(". Generated classes that satisfy this relationship for all the data sources that were defined in C# may be found in the "),F,e(" property on an instance of a ViewModel or ListViewModel, or in "),T]),I]),_:1}),x,s("p",null,[e("You can setup "),a(o,{to:"/stacks/disambiguation/list-view-model.html"},{default:l(()=>[e("TypeScript List ViewModels")]),_:1}),e(" to automatically reload from the server when data source parameters change:")]),a(i,null,{vue:l(()=>[s("p",null,[e("To automatically reload a "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[e("ListViewModel")]),_:1}),e(" when data source parameters change, simply use the list's "),P,e(" or "),L,e(" function:")]),k]),knockout:l(()=>[B,M]),_:1}),Q,q,G,s("p",null,[e("When an object or list of objects is requested, the default behavior of the the "),V,e(" is to load all of the immediate relationships of the object (parent objects and child collections), as well as the far side of "),a(o,{to:"/modeling/model-components/attributes/many-to-many.html"},{default:l(()=>[e("many-to-many")]),_:1}),e(" relationships. This is performed in "),W,e(", so in order to suppress this behavior in a custom data source, don't build you query off of "),O,e(", but instead start directly from the "),N,e(" for your entity when building your custom query.")]),s("p",null,[e("Clients can suppress this per-request by setting "),R,e(" on your TypeScript "),a(o,{to:"/stacks/disambiguation/view-model.html"},{default:l(()=>[e("ViewModel")]),_:1}),e(" or "),a(o,{to:"/stacks/disambiguation/list-view-model.html"},{default:l(()=>[e("ListViewModel")]),_:1}),e(", but note this is not a security mechanism and should only be used to reduce payload size or improve response time.")]),j,z,$,a(n,{def:"CrudContext Context"}),U,a(n,{def:"TContext Db"}),Y,a(n,{def:"ClaimsPrincipal User"}),J,a(n,{def:"int MaxSearchTerms"}),K,a(n,{def:"int DefaultPageSize"}),X,a(n,{def:"int MaxPageSize"}),H,a(n,{def:`IQueryable GetQuery(IDataSourceParameters parameters); +Task> GetQueryAsync(IDataSourceParameters parameters);`}),Z,s("div",ss,[es,s("p",null,[e("When "),as,e(" is overridden, the "),a(o,{to:"/modeling/model-components/data-sources.html#default-loading-behavior"},{default:l(()=>[e("Default Loading Behavior")]),_:1}),e(" is overridden as well. To restore this behavior, use the "),ls,e(" extension method to build your query.")])]),a(n,{def:"IncludeTree? GetIncludeTree(IQueryable query, IDataSourceParameters parameters)"}),s("p",null,[e("Allows for explicitly specifying the "),a(o,{to:"/concepts/include-tree.html"},{default:l(()=>[e("Include Tree")]),_:1}),e(" that will be used when serializing results obtained from this data source into DTOs. By default, the query that is build up through all the other methods in the data source will be used to build the include tree.")]),a(n,{def:"bool CanEvalQueryAsynchronously(IQueryable query)"}),ns,a(n,{def:"IQueryable ApplyListFiltering(IQueryable query, IFilterParameters parameters)"}),os,a(n,{def:"IQueryable ApplyListPropertyFilters(IQueryable query, IFilterParameters parameters)"}),ts,a(n,{def:"IQueryable ApplyListPropertyFilter(IQueryable query, PropertyViewModel prop, string value)"}),rs,c(" MARKER:filter-behaviors "),ps,c(" MARKER:end-filter-behaviors "),a(n,{def:"IQueryable ApplyListSearchTerm(IQueryable query, IFilterParameters parameters)"}),s("p",null,[e("Applies filters to the query based on the specified search term. See "),a(o,{to:"/modeling/model-components/attributes/search.html"},{default:l(()=>[e("[Search]")]),_:1}),e(" for a detailed look at how searching works in Coalesce.")]),a(n,{def:"IQueryable ApplyListSorting(IQueryable query, IListParameters parameters)"}),cs,a(n,{def:"IQueryable ApplyListClientSpecifiedSorting(IQueryable query, IListParameters parameters)"}),is,a(n,{def:"IQueryable ApplyListDefaultSorting(IQueryable query)"}),Ds,c(" .. TODO - need a centralized doc page about sorting in Coalesce. "),a(n,{def:"IQueryable ApplyListPaging(IQueryable query, IListParameters parameters, int? totalCount, out int page, out int pageSize)"}),ds,a(n,{def:"Task GetListTotalCountAsync(IQueryable query, IFilterParameters parameters)"}),ys,a(n,{def:`void TransformResults(IReadOnlyList results, IDataSourceParameters parameters); +Task TransformResultsAsync(IReadOnlyList results, IDataSourceParameters parameters);`}),us,s("p",null,[e("This method is only called immediately before mapping to a DTO; it does not affect operations that don't involve mapping to a DTO - e.g. when loading the target of a "),hs,e(" operation or when loading the invocation target of an "),a(o,{to:"/modeling/model-components/methods.html#instance-methods"},{default:l(()=>[e("instance method")]),_:1}),e(".")]),s("p",null,[e("See the "),a(o,{to:"/topics/security.html#transform-results"},{default:l(()=>[e("Security")]),_:1}),e(" page for an example on how to use TransformResults to "),a(o,{to:"/topics/security.html#transform-results"},{default:l(()=>[e("apply filtered includes")]),_:1}),e(".")]),Cs,a(n,{def:"IList TrimListFields(IList mappedResult, IListParameters parameters)"}),ms])}const Es=D(u,[["render",vs],["__file","data-sources.html.vue"]]);export{Es as default}; diff --git a/assets/date-type.html.ae667ab6.js b/assets/date-type.html.ae667ab6.js new file mode 100644 index 000000000..a58f9bb8e --- /dev/null +++ b/assets/date-type.html.ae667ab6.js @@ -0,0 +1,8 @@ +import{_ as n,y as l,z as t,Q as p,B as e,a5 as o,X as s,P as r}from"./framework.fe9a73df.js";const c={},D=o(`

[DateType]

Specifies whether a DateTime type will have a date and a time, or only a date.

Example Usage

public class Person
+{
+    public int PersonId { get; set; }
+
+    [DateType(DateTypeAttribute.DateTypes.DateOnly)]
+    public DateTimeOffset? BirthDate { get; set; }
+}
+

Properties

`,5),i=s("p",null,"The type of date the property represents.",-1),d=s("p",null,"Enum values are:",-1),y=s("ul",null,[s("li",null,[s("code",null,"DateTypeAttribute.DateTypes.DateTime"),e(" Subject is both a date and time.")]),s("li",null,[s("code",null,"DateTypeAttribute.DateTypes.DateOnly"),e(" Subject is only a date with no significant time component.")])],-1);function u(h,m){const a=r("Prop");return l(),t("div",null,[D,p(a,{def:"public DateTypes DateType { get; set; } = DateTypes.DateTime; ",ctor:"1"}),e(),i,d,y])}const v=n(c,[["render",u],["__file","date-type.html.vue"]]);export{v as default}; diff --git a/assets/date-type.html.b681aadc.js b/assets/date-type.html.b681aadc.js new file mode 100644 index 000000000..11c9932f4 --- /dev/null +++ b/assets/date-type.html.b681aadc.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-2ddfa8c0","path":"/modeling/model-components/attributes/date-type.html","title":"[DateType]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/date-type.md"}');export{e as data}; diff --git a/assets/default-order-by.html.156c96e7.js b/assets/default-order-by.html.156c96e7.js new file mode 100644 index 000000000..128d973e9 --- /dev/null +++ b/assets/default-order-by.html.156c96e7.js @@ -0,0 +1,25 @@ +import{_ as p,y as t,z as r,X as s,B as n,Q as e,$ as D,a5 as c,P as a}from"./framework.fe9a73df.js";const i={},d=s("h1",{id:"defaultorderby",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#defaultorderby","aria-hidden":"true"},"#"),n(" [DefaultOrderBy]")],-1),y=s("p",null,"Allows setting of the default manner in which the data returned to the client will be sorted. Multiple fields can be used to sort an object by specifying an index.",-1),u=s("p",null,"This affects the sort order both when requesting a list of the model itself, as well as when the model appears as a child collection off of a navigation property of another object.",-1),C=s("code",null,"orderBy",-1),h=s("code",null,"orderByDescending",-1),b=s("code",null,"ListViewModel",-1),m=c(`

Example Usage

public class Person
+{
+    public int PersonId { get; set; }
+    
+    public int DepartmentId { get; set; }
+
+    [DefaultOrderBy(FieldOrder = 0, FieldName = nameof(Department.Order))]
+    public Department Department { get; set; }
+    
+    [DefaultOrderBy(FieldOrder = 1)]
+    public string LastName { get; set; }
+}
+
public class Person
+{
+    public int PersonId { get; set; }
+    
+    public int DepartmentId { get; set; }
+
+    [DefaultOrderBy(FieldOrder = 0, FieldName = nameof(Department.Order))]
+    public Department Department { get; set; }
+    
+    [DefaultOrderBy(FieldOrder = 1)]
+    public string LastName { get; set; }
+}
+

Properties

`,4),v=s("p",null,"Specify the index of this field when sorting by multiple fields.",-1),f=s("p",null,[n("Lower-valued properties will be used first; higher-valued properties will be used as a tiebreaker (i.e. "),s("code",null,".ThenBy(...)"),n(").")],-1),_=s("p",null,"Specify the direction of the ordering for the property.",-1),E=s("p",null,"Enum values are:",-1),g=s("ul",null,[s("li",null,[s("code",null,"DefaultOrderByAttribute.OrderByDirections.Ascending")]),s("li",null,[s("code",null,"DefaultOrderByAttribute.OrderByDirections.Descending")])],-1),B=s("p",null,[n("When using the "),s("code",null,"DefaultOrderByAttribute"),n(" on an object property, specifies the field on the object to use for sorting. See the first example above.")],-1);function F(O,x){const o=a("RouterLink"),l=a("Prop");return t(),r("div",null,[d,y,u,s("p",null,[n("In the first case (a list of the model itself), this can be overridden by setting the "),C,n(" or "),h,n(" property on the TypeScript "),b,n(" - see "),e(o,{to:"/stacks/disambiguation/list-view-model.html"},{default:D(()=>[n("TypeScript List ViewModels")]),_:1}),n(".")]),m,e(l,{def:"public int FieldOrder { get; set; } = 0; ",ctor:"1"}),n(),v,f,e(l,{def:"public OrderByDirections OrderByDirection { get; set; } = OrderByDirections.Ascending;",ctor:"2"}),_,E,g,e(l,{def:"public string FieldName { get; set; }"}),B])}const k=p(i,[["render",F],["__file","default-order-by.html.vue"]]);export{k as default}; diff --git a/assets/default-order-by.html.638f5e51.js b/assets/default-order-by.html.638f5e51.js new file mode 100644 index 000000000..c160f2000 --- /dev/null +++ b/assets/default-order-by.html.638f5e51.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-28165697","path":"/modeling/model-components/attributes/default-order-by.html","title":"[DefaultOrderBy]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/default-order-by.md"}');export{e as data}; diff --git a/assets/dto-includes-excludes.html.221ebe75.js b/assets/dto-includes-excludes.html.221ebe75.js new file mode 100644 index 000000000..f4129b8ae --- /dev/null +++ b/assets/dto-includes-excludes.html.221ebe75.js @@ -0,0 +1,43 @@ +import{_ as r,y as i,z as D,X as s,B as e,Q as l,$ as n,a5 as t,P as a}from"./framework.fe9a73df.js";const d={},y=s("h1",{id:"dtoincludes-dtoexcludes",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#dtoincludes-dtoexcludes","aria-hidden":"true"},"#"),e(" [DtoIncludes] & [DtoExcludes]")],-1),u=s("p",null,[e("Allows for easily controlling what data gets set to the client. When requesting data from the generated client-side list view models, you can specify an "),s("code",null,"includes"),e(" property on the ViewModel or ListViewModel.")],-1),C=s("p",null,[e("When the database entries are returned to the client they will be trimmed based on the requested includes string and the values in "),s("code",null,"DtoExcludes"),e(" and "),s("code",null,"DtoIncludes"),e(".")],-1),m={class:"custom-container danger"},h=s("p",{class:"custom-container-title"},"DANGER",-1),b=s("p",null,[e("These attributes are "),s("strong",null,"not security attributes"),e(" - consumers of your application's API can set the includes string to any value when making a request.")],-1),v=s("p",null,[e("It is important to note that the value of the includes string will match against these attributes on "),s("em",null,"any"),e(" of your models that appears in the object graph being mapped to DTOs - it is not limited only to the model type of the root object.")],-1),E={class:"custom-container tip"},_=s("p",{class:"custom-container-title"},"Important",-1),w=s("code",null,"DtoIncludes",-1),g=s("em",null,"permits",-1),f=s("em",null,"already",-1),A=t(`

Example Usage

Server code:

public class Person
+{
+    // Don't include CreatedBy when editing - will be included for all other views
+    [DtoExcludes("Editor")]
+    public AppUser CreatedBy { get; set; }
+
+    // Only include the Person's Department when \`includes == "details"\` on the TypeScript ViewModel.
+    [DtoIncludes("details")]
+    public Department Department { get; set; }
+
+    // LastName will be included in all views
+    public string LastName { get; set; }
+}
+
+public class Department
+{
+    [DtoIncludes("details")]
+    public ICollection<Person> People { get; set; }
+}
+

Client code:

`,4),F=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#4FC1FF"}},"personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList.$items will not contain CreatedBy nor Department objects.")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#4FC1FF"}},"personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. ")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Department will be allowed to include its other Person objects.")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),L=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#6A9955"}},"// objects in personList.items will not contain CreatedBy nor Department objects.")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#6A9955"}},"// objects in personList2.items will be allowed to contain both CreatedBy and Department objects. Department will be allowed to include its other Person objects.")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),x=s("h2",{id:"properties",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#properties","aria-hidden":"true"},"#"),e(" Properties")],-1),B=s("code",null,"includes",-1),P=t("

For DtoIncludes, this will be the values of includes for which this property will be allowed to be serialized and sent to the client.

For DtoExcludes, this will be the values of includes for which this property will not be serialized and sent to the client.

",2);function k(I,V){const o=a("RouterLink"),c=a("CodeTabs"),p=a("Prop");return i(),D("div",null,[y,u,s("p",null,[e("For more information about the includes string, see "),l(o,{to:"/concepts/includes.html"},{default:n(()=>[e("Includes String")]),_:1}),e(".")]),C,s("div",m,[h,b,s("p",null,[e("Do not use them to keep certain data private - use the "),l(o,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:n(()=>[e("Security Attributes")]),_:1}),e(" family of attributes for that.")])]),v,s("div",E,[_,s("p",null,[w,e(" does not ensure that specific data will be loaded from the database. It only "),g,e(" what is "),f,e(" loaded into the current EF DbContext to be returned from the API. See "),l(o,{to:"/modeling/model-components/data-sources.html"},{default:n(()=>[e("Data Sources")]),_:1}),e(" to learn how to control what data gets loaded from the database.")])]),A,l(c,null,{vue:n(()=>[F]),knockout:n(()=>[L]),_:1}),x,l(p,{def:"public string ContentViews { get; set; }",ctor:"1"}),e(),s("p",null,[e("A comma-delimited list of values of "),l(o,{to:"/concepts/includes.html"},{default:n(()=>[B]),_:1}),e(" on which to operate.")]),P])}const q=r(d,[["render",k],["__file","dto-includes-excludes.html.vue"]]);export{q as default}; diff --git a/assets/dto-includes-excludes.html.5a9182f5.js b/assets/dto-includes-excludes.html.5a9182f5.js new file mode 100644 index 000000000..edc8b64b6 --- /dev/null +++ b/assets/dto-includes-excludes.html.5a9182f5.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-ee3c31e8","path":"/modeling/model-components/attributes/dto-includes-excludes.html","title":"[DtoIncludes] & [DtoExcludes]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/dto-includes-excludes.md"}');export{e as data}; diff --git a/assets/dtos.html.01f136aa.js b/assets/dtos.html.01f136aa.js new file mode 100644 index 000000000..687e7e21b --- /dev/null +++ b/assets/dtos.html.01f136aa.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-94b3a3fa","path":"/stacks/agnostic/dtos.html","title":"Generated C# DTOs","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Automatically Generated DTOs","slug":"automatically-generated-dtos","link":"#automatically-generated-dtos","children":[]}],"git":{"updatedTime":1678412142000},"filePathRelative":"stacks/agnostic/dtos.md"}');export{t as data}; diff --git a/assets/dtos.html.7540bee6.js b/assets/dtos.html.7540bee6.js new file mode 100644 index 000000000..601bbd1d7 --- /dev/null +++ b/assets/dtos.html.7540bee6.js @@ -0,0 +1 @@ +import{_ as r,y as l,z as n,X as t,B as e,Q as s,$ as o,P as d}from"./framework.fe9a73df.js";const i={},c=t("h1",{id:"generated-c-dtos",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#generated-c-dtos","aria-hidden":"true"},"#"),e(" Generated C# DTOs")],-1),u=t("p",null,"Data Transfer Objects, or DTOs, allow for transformations of data from the data store into a format more suited for transfer and use on the client side. This often means trimming properties and flattening structures to provide a leaner over-the-wire experience. Coalesce aims to support this as seamlessly as possible.",-1),h=t("p",null,"Coalesce supports two types of DTOs:",-1),m=t("h2",{id:"automatically-generated-dtos",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#automatically-generated-dtos","aria-hidden":"true"},"#"),e(" Automatically Generated DTOs")],-1),p=t("p",null,"Every class that is exposed through Coalesce's generated API will have a corresponding DTO generated for it. These DTOs are used to shuttle data back and forth to the client. They are generated classes that have nullable versions of all the properties on the POCO class.",-1);function f(_,b){const a=d("RouterLink");return l(),n("div",null,[c,u,h,t("ul",null,[t("li",null,[e("DTOs that are automatically generated for each POCO database object. These are controlled via "),s(a,{to:"/modeling/model-components/attributes.html"},{default:o(()=>[e("Attributes")]),_:1}),e(" on the POCO. These are outlined below.")]),t("li",null,[e("DTOs that you create with IClassDto. These are outlined at "),s(a,{to:"/modeling/model-types/dtos.html"},{default:o(()=>[e("Custom DTOs")]),_:1}),e(".")])]),m,p,t("p",null,[s(a,{to:"/modeling/model-components/attributes/dto-includes-excludes.html"},{default:o(()=>[e("[DtoIncludes] & [DtoExcludes]")]),_:1}),e(" and the "),s(a,{to:"/concepts/includes.html"},{default:o(()=>[e("Includes String")]),_:1}),e(" infrastructure can be used to indicate which properties should be transferred to the client in which cases, and "),s(a,{to:"/concepts/include-tree.html"},{default:o(()=>[e("Include Tree")]),_:1}),e(" is used to dictate how these DTOs are constructed from POCOs retrieved from the database.")]),t("p",null,[e("The "),s(a,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:o(()=>[e("[Read] and [Edit] attributes")]),_:1}),e(" can be used to apply property-level security, which manifests as conditional logic in the mapping methods on the generated DTOs.")]),t("p",null,[e("See the "),s(a,{to:"/topics/security.html#attributes"},{default:o(()=>[e("Security")]),_:1}),e(" page to read more about property-level security, as well as all other security mechanisms in Coalesce.")])])}const T=r(i,[["render",f],["__file","dtos.html.vue"]]);export{T as default}; diff --git a/assets/dtos.html.897f99f4.js b/assets/dtos.html.897f99f4.js new file mode 100644 index 000000000..9731b6142 --- /dev/null +++ b/assets/dtos.html.897f99f4.js @@ -0,0 +1,91 @@ +import{_ as D,y as r,z as i,X as n,B as s,Q as a,$ as l,W as t,a5 as p,P as c}from"./framework.fe9a73df.js";const d={},y=n("h1",{id:"custom-dtos",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#custom-dtos","aria-hidden":"true"},"#"),s(" Custom DTOs")],-1),u=n("code",null,"IClassDto",-1),C={class:"table-of-contents"},m=n("p",null,"The difference between a Custom DTO and the underlying entity that they represent is as follows:",-1),v=n("h2",{id:"creating-a-custom-dto",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#creating-a-custom-dto","aria-hidden":"true"},"#"),s(" Creating a Custom DTO")],-1),h=n("code",null,"IClassDTO",-1),b=n("code",null,"T",-1),g=n("code",null,"DbSet",-1),E=n("code",null,"DbContext",-1),f=n("code",null,"DbContext",-1),_=n("code",null,"IClassDTO",-1),T=p(`

Next, ensure that one property is annotated with [Key] so that Coalesce can know the primary key of your DTO in order to perform database lookups and keep track of your object uniquely in the client-side TypeScript.

Now, populate the required MapTo and MapFrom methods with code for mapping from and to your DTO, respectively (the methods are named with respect to the underlying entity, not the DTO). Most properties probably map one-to-one in both directions, but you probably created a DTO because you wanted some sort of custom mapping - say, mapping a collection on your entity with a comma-delimited string on the DTO. This is also the place to perform any user-based, role-based, property-level security. You can access the current user on the IMappingContext object.

[Coalesce]
+public class CaseDto : IClassDto<Case>
+{
+    [Key]
+    public int CaseId { get; set; }
+
+    public string Title { get; set; }
+
+    [Read]
+    public string AssignedToName { get; set; }
+
+    public void MapTo(Case obj, IMappingContext context)
+    {
+        obj.Title = Title;
+    }
+
+    public void MapFrom(Case obj, IMappingContext context = null, IncludeTree tree = null)
+    {
+        CaseId = obj.CaseKey;
+        Title = obj.Title;
+        if (obj.AssignedTo != null)
+        {
+            AssignedToName = obj.AssignedTo.Name;
+        }
+    }
+}
+
`,3),F={class:"custom-container warning"},B=n("p",{class:"custom-container-title"},"WARNING",-1),w=n("code",null,"MapTo",-1),x=n("code",null,"MapFrom",-1),I=n("code",null,"Mapper",-1),S=n("code",null,"IncludeTree",-1),k=n("code",null,"IncludeTree",-1),A=n("code",null,"IncludeTree",-1),O=n("code",null,"null",-1),j=n("code",null,"Mapper",-1),M=n("code",null,"IncludeTree",-1),P=p(`
using IntelliTect.Coalesce.Mapping;
+
+[Coalesce]
+public class CaseDto : IClassDto<Case>
+{
+    public int ProductId { get; set; }
+    public Product Product { get; set; }
+    ...
+
+    public void MapFrom(Case obj, IMappingContext context = null, IncludeTree tree = null)
+    {
+        ProductId = obj.ProductId;
+
+        if (tree == null || tree[nameof(this.Product)] != null)
+            Product = Mapper.MapToDto<Product, ProductDtoGen>(obj.Product, context, tree?[nameof(this.Product)]
+        ...
+    }
+}
+

Using Custom DataSources and Behaviors

Declaring an IClassDto DataSource

`,3),N=p(`
  1. As a nested class of the DTO. The relationship between your data source or behaviors and your DTO will be picked up automatically.

    [Coalesce]
    +public class CaseDto : IClassDto<Case>
    +{
    +    [Key]
    +    public int CaseId { get; set; }
    +
    +    public string Title { get; set; }
    +    
    +    ...
    +
    +    public class MyCaseDtoSource : StandardDataSource<Case, AppDbContext>
    +    {
    +        ...
    +    }
    +}
    +
  2. With a [DeclaredFor] attribute that references the DTO type:

    [Coalesce]
    +public class CaseDto : IClassDto<Case>
    +{
    +    [Key]
    +    public int CaseId { get; set; }
    +
    +    public string Title { get; set; }
    +    
    +    ...
    +}
    +
    +[Coalesce, DeclaredFor(typeof(CaseDto))]
    +public class MyCaseDtoSource : StandardDataSource<Case, AppDbContext>
    +{
    +    ...
    +}
    +

ProjectedDtoDataSource

`,2),V=n("code",null,"ProjectedDtoDataSource",-1),K=n("code",null,"ProjectedDtoDataSource",-1),G=p(`
[Coalesce, DeclaredFor(typeof(CaseDto))]
+public class CaseDtoSource : ProjectedDtoDataSource<Case, CaseDto, AppDbContext>
+{
+    public CaseDtoSource(CrudContext<AppDbContext> context) : base(context) { }
+
+    public override IQueryable<CaseDto> ApplyProjection(IQueryable<Case> query, IDataSourceParameters parameters)
+    {
+        return query.Select(c => new CaseDto
+        {
+            CaseId = c.CaseKey,
+            Title = c.Title,
+            AssignedToName = c.AssignedTo == null ? null : c.AssignedTo.Name
+        });
+    }
+}
+

Surgical Saves

`,2),R=n("code",null,"$saveMode",-1),q=n("code",null,"saveIncludedFields",-1),W=n("p",null,[s("Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the "),n("code",null,"x-www-form-urlencoded"),s(" body that is sent to the server.")],-1);function z(L,Q){const e=c("RouterLink"),o=c("router-link");return r(),i("div",null,[y,n("p",null,[s("In addition to the generated "),a(e,{to:"/stacks/agnostic/dtos.html"},{default:l(()=>[s("Generated C# DTOs")]),_:1}),s(" that Coalesce will create for you, you may also create your own implementations of an "),u,s(". These types are first-class citizens in Coalesce - you will get a full suite of features surrounding them as if they were entities. This includes generated API Controllers, Admin Views, and full "),a(e,{to:"/stacks/disambiguation/view-model.html"},{default:l(()=>[s("TypeScript ViewModels")]),_:1}),s(" and "),a(e,{to:"/stacks/disambiguation/list-view-model.html"},{default:l(()=>[s("TypeScript List ViewModels")]),_:1}),s(".")]),n("nav",C,[n("ul",null,[n("li",null,[a(o,{to:"#creating-a-custom-dto"},{default:l(()=>[s("Creating a Custom DTO")]),_:1})]),n("li",null,[a(o,{to:"#using-custom-datasources-and-behaviors"},{default:l(()=>[s("Using Custom DataSources and Behaviors")]),_:1}),n("ul",null,[n("li",null,[a(o,{to:"#declaring-an-iclassdto-datasource"},{default:l(()=>[s("Declaring an IClassDto DataSource")]),_:1})]),n("li",null,[a(o,{to:"#projecteddtodatasource"},{default:l(()=>[s("ProjectedDtoDataSource")]),_:1})])])]),n("li",null,[a(o,{to:"#surgical-saves"},{default:l(()=>[s("Surgical Saves")]),_:1})])])]),m,n("ul",null,[n("li",null,[n("p",null,[s("The only time your custom DTO will be served is when it is requested directly from one of the endpoints on its generated controller, or when its type is explicitly used by a "),a(e,{to:"/modeling/model-components/methods.html"},{default:l(()=>[s("method")]),_:1}),s(" or "),a(e,{to:"/modeling/model-components/properties.html"},{default:l(()=>[s("property")]),_:1}),s(" of another type.")])]),n("li",null,[n("p",null,[s("When mapping data from your database, or mapping data incoming from the client, the DTO itself must manually map all properties, since there is no corresponding "),a(e,{to:"/stacks/agnostic/dtos.html"},{default:l(()=>[s("Generated DTO")]),_:1}),s(". Attributes like "),a(e,{to:"/modeling/model-components/attributes/dto-includes-excludes.html"},{default:l(()=>[s("[DtoIncludes] & [DtoExcludes]")]),_:1}),s(" and property-level security through "),a(e,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:l(()=>[s("Security Attributes")]),_:1}),s(" have no effect on custom DTOs, since those attribute only affect what get generated for "),a(e,{to:"/stacks/agnostic/dtos.html"},{default:l(()=>[s("Generated C# DTOs")]),_:1}),s(".")])])]),v,n("p",null,[s("To create a custom DTO, define a class annotated with "),a(e,{to:"/modeling/model-components/attributes/coalesce.html"},{default:l(()=>[s("[Coalesce]")]),_:1}),s(" that implements "),h,s(", where "),b,s(" is an EF Core POCO with a corresponding "),g,s(" on a "),E,s(" that has also been exposed with "),a(e,{to:"/modeling/model-components/attributes/coalesce.html"},{default:l(()=>[s("[Coalesce]")]),_:1}),s(". Add any "),a(e,{to:"/modeling/model-components/properties.html"},{default:l(()=>[s("Properties")]),_:1}),s(" to it just as you would add "),a(e,{to:"/modeling/model-components/properties.html"},{default:l(()=>[s("model properties")]),_:1}),s(" to a regular EF model. If you are not exposing a "),f,s(" class with "),a(e,{to:"/modeling/model-components/attributes/coalesce.html"},{default:l(()=>[s("[Coalesce]")]),_:1}),s(" but still wish to create a Custom DTO based upon one of its entities, you can inherit from "),_,s(" instead as a means of explicitly declaring the type of the DbContext.")]),T,n("div",F,[B,n("p",null,[s("Custom DTOs do not utilize property-level "),a(e,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:l(()=>[s("Security Attributes")]),_:1}),s(" nor "),a(e,{to:"/modeling/model-components/attributes/dto-includes-excludes.html"},{default:l(()=>[s("[DtoIncludes] & [DtoExcludes]")]),_:1}),s(", since these are handled in the "),a(e,{to:"/stacks/agnostic/dtos.html"},{default:l(()=>[s("Generated DTOs")]),_:1}),s(". If you need property-level security or trimming, you must write it yourself in the "),w,s(" and "),x,s(" methods.")])]),n("p",null,[s("If you have any child objects on your DTO, you can invoke the mapper for some other object using the static "),I,s(" class. Also seen in this example is how to respect the "),a(e,{to:"/concepts/include-tree.html"},{default:l(()=>[s("Include Tree")]),_:1}),s(" when mapping entity types; however, respecting the "),S,s(" is optional. Since this DTO is a custom type that you've written, if you're certain your use cases don't need to worry about object graph trimming, then you can ignore the "),k,s(". If you do ignore the "),A,s(", you should pass "),O,s(" to calls to "),j,s(" - don't pass in the incoming "),M,s(", as this could cause unexpected results.")]),P,n("p",null,[s("When you create a custom DTO, it will use the "),a(e,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[s("Standard Data Source")]),_:1}),s(" and "),a(e,{to:"/modeling/model-components/behaviors.html#standard-behaviors"},{default:l(()=>[s("Standard Behaviors")]),_:1}),s(" just like any of your regular "),a(e,{to:"/modeling/model-types/entities.html"},{default:l(()=>[s("Entity Models")]),_:1}),s(". If you wish to override this, your custom data source and/or behaviors MUST be declared in one of the following ways:")]),N,n("p",null,[s("In addition to creating a "),a(e,{to:"/modeling/model-components/data-sources.html"},{default:l(()=>[s("Data Source")]),_:1}),s(" by deriving from "),a(e,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[s("Standard Data Source")]),_:1}),s(", there also exists a class "),V,s(" that can be used to easily perform projection from EF model types to your custom DTO types using EF query projections. "),K,s(" inherits from "),a(e,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[s("Standard Data Source")]),_:1}),s(".")]),G,n("p",null,[s("The "),a(e,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[s("Vue ViewModels")]),_:1}),s(" support surgical saves through their "),R,s(" property, and "),a(e,{to:"/stacks/ko/client/view-model.html"},{default:l(()=>[s("Knockout ViewModels")]),_:1}),s(" through the "),a(e,{to:"/stacks/ko/client/model-config.html#viewmodelconfiguration"},{default:l(()=>[q,s(" configuration")]),_:1}),s(".")]),t(" MARKER:surgical-saves-warning "),W,n("p",null,[s("The "),a(e,{to:"/stacks/agnostic/dtos.html"},{default:l(()=>[s("Generated C# DTOs")]),_:1}),s(" implement the necessary logic for this; however, any "),a(e,{to:"/modeling/model-types/dtos.html"},{default:l(()=>[s("Custom DTOs")]),_:1}),s(" must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the "),a(e,{to:"/stacks/agnostic/dtos.html"},{default:l(()=>[s("Generated C# DTOs")]),_:1}),s(", or do not use surgical saves with Custom DTOs.")]),t(" MARKER:end-surgical-saves-warning ")])}const $=D(d,[["render",z],["__file","dtos.html.vue"]]);export{$ as default}; diff --git a/assets/dtos.html.c910d3c7.js b/assets/dtos.html.c910d3c7.js new file mode 100644 index 000000000..cb6c45c42 --- /dev/null +++ b/assets/dtos.html.c910d3c7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-d0002c24","path":"/modeling/model-types/dtos.html","title":"Custom DTOs","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Custom DTO","slug":"creating-a-custom-dto","link":"#creating-a-custom-dto","children":[]},{"level":2,"title":"Using Custom DataSources and Behaviors","slug":"using-custom-datasources-and-behaviors","link":"#using-custom-datasources-and-behaviors","children":[{"level":3,"title":"Declaring an IClassDto DataSource","slug":"declaring-an-iclassdto-datasource","link":"#declaring-an-iclassdto-datasource","children":[]},{"level":3,"title":"ProjectedDtoDataSource","slug":"projecteddtodatasource","link":"#projecteddtodatasource","children":[]}]},{"level":2,"title":"Surgical Saves","slug":"surgical-saves","link":"#surgical-saves","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-types/dtos.md"}');export{e as data}; diff --git a/assets/entities.html.1679e7fa.js b/assets/entities.html.1679e7fa.js new file mode 100644 index 000000000..c907d5298 --- /dev/null +++ b/assets/entities.html.1679e7fa.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5f0df441","path":"/modeling/model-types/entities.html","title":"Entity Models","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Building a Data Model","slug":"building-a-data-model","link":"#building-a-data-model","children":[{"level":3,"title":"Properties","slug":"properties","link":"#properties","children":[]},{"level":3,"title":"Attributes","slug":"attributes","link":"#attributes","children":[]},{"level":3,"title":"Methods","slug":"methods","link":"#methods","children":[]}]},{"level":2,"title":"Customizing CRUD Operations","slug":"customizing-crud-operations","link":"#customizing-crud-operations","children":[{"level":3,"title":"Data Sources","slug":"data-sources","link":"#data-sources","children":[]},{"level":3,"title":"Behaviors","slug":"behaviors","link":"#behaviors","children":[]}]},{"level":2,"title":"Standalone (non-EF) Entities","slug":"standalone-non-ef-entities","link":"#standalone-non-ef-entities","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-types/entities.md"}');export{e as data}; diff --git a/assets/entities.html.925a4840.js b/assets/entities.html.925a4840.js new file mode 100644 index 000000000..8f5217c85 --- /dev/null +++ b/assets/entities.html.925a4840.js @@ -0,0 +1,49 @@ +import{_ as c,y as D,z as i,X as a,B as s,Q as n,$ as e,a5 as r,P as p}from"./framework.fe9a73df.js";const d={},y=a("h1",{id:"entity-models",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#entity-models","aria-hidden":"true"},"#"),s(" Entity Models")],-1),u={href:"https://docs.microsoft.com/en-us/ef/core/",target:"_blank",rel:"noopener noreferrer"},C={class:"table-of-contents"},m=a("h2",{id:"building-a-data-model",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#building-a-data-model","aria-hidden":"true"},"#"),s(" Building a Data Model")],-1),h={href:"https://docs.microsoft.com/en-us/ef/core/",target:"_blank",rel:"noopener noreferrer"},v={href:"https://docs.microsoft.com/en-us/ef/core/",target:"_blank",rel:"noopener noreferrer"},b=a("p",null,[s("Don't worry about querying or saving data when you're just getting started - Coalesce will provide a lot of that functionality for you, and it is very easy to customize what Coalesce offers later. To get started, just build your POCOs and "),a("code",null,"DbContext"),s(" classes. Annotate your "),a("code",null,"DbContext"),s(" class with "),a("code",null,"[Coalesce]"),s(" so that Coalesce will discover it and generate code based off of your context for you.")],-1),E=a("p",null,"Before you start building, you are highly encouraged to read the sections below. The linked pages explain in greater detail what Coalesce will build for you for each part of your data model.",-1),f=a("h3",{id:"properties",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#properties","aria-hidden":"true"},"#"),s(" Properties")],-1),g=a("h3",{id:"attributes",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#attributes","aria-hidden":"true"},"#"),s(" Attributes")],-1),_=a("p",null,[s("Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from "),a("code",null,"System.ComponentModel.DataAnnotations"),s(".")],-1),F=a("h3",{id:"methods",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#methods","aria-hidden":"true"},"#"),s(" Methods")],-1),B=r('

Customizing CRUD Operations

Once you've got a solid data model in place, its time to start customizing the way that Coalesce will read your data, as well as the way that it will handle your data when processing creates, updates, and deletes.

Data Sources

',3),S=a("h3",{id:"behaviors",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#behaviors","aria-hidden":"true"},"#"),s(" Behaviors")],-1),A=a("h2",{id:"standalone-non-ef-entities",tabindex:"-1"},[a("a",{class:"header-anchor",href:"#standalone-non-ef-entities","aria-hidden":"true"},"#"),s(" Standalone (non-EF) Entities")],-1),x=a("p",null,[s("In Coalesce, Standalone Entities are entity types that are not based on Entity Framework. These types are discovered by Coalesce by annotating them with "),a("code",null,"[Coalesce, StandaloneEntity]"),s(".")],-1),w=r(`

To define data sources and behaviors for Standalone Entities, it is recommended you inherit from StandardDataSource<T> and StandardBehaviors<T>, respectively. For example:

[Coalesce, StandaloneEntity]
+public class StandaloneExample
+{
+    public int Id { get; set; }
+
+    [Search(SearchMethod = SearchAttribute.SearchMethods.Contains), ListText]
+    public string Name { get; set; } = "";
+
+    [DefaultOrderBy]
+    public DateTimeOffset Date { get; set; }
+
+    private static int nextId = 0;
+    private static ConcurrentDictionary<int, StandaloneExample> backingStore = new ConcurrentDictionary<int, StandaloneExample>();
+
+    public class DefaultSource : StandardDataSource<StandaloneExample>
+    {
+        public DefaultSource(CrudContext context) : base(context) { }
+
+        public override Task<IQueryable<StandaloneExample>> GetQueryAsync(IDataSourceParameters parameters)
+            => Task.FromResult(backingStore.Values.AsQueryable());
+    }
+
+    public class Behaviors : StandardBehaviors<StandaloneExample>
+    {
+        public Behaviors(CrudContext context) : base(context) { }
+
+        public override Task ExecuteDeleteAsync(StandaloneExample item)
+        {
+            backingStore.TryRemove(item.Id, out _);
+            return Task.CompletedTask;
+        }
+
+        public override Task ExecuteSaveAsync(SaveKind kind, StandaloneExample? oldItem, StandaloneExample item)
+        {
+            if (kind == SaveKind.Create)
+            {
+                item.Id = Interlocked.Increment(ref nextId);
+                backingStore.TryAdd(item.Id, item);
+            }
+            else
+            {
+                backingStore.TryRemove(item.Id, out _);
+                backingStore.TryAdd(item.Id, item);
+            }
+            return Task.CompletedTask;
+        }
+    }
+}
+

The above example is admittedly contrived, as it is unlikely that you would be using an in-memory collection as a data persistence mechanism. A more likely real-world scenario would be to inject an interface to some other data store. Data Source and Behavior classes are instantiated using your application's service provider, so any registered service can be injected.

`,3);function k(T,I){const t=p("ExternalLinkIcon"),o=p("router-link"),l=p("RouterLink");return D(),i("div",null,[y,a("p",null,[s("Models are the core business objects of your application - they serve as the fundamental representation of data in your application. The design of your models is very important. In "),a("a",u,[s("Entity Framework Core"),n(t)]),s(" (EF), data models are just Plain Old CLR Objects (POCOs).")]),a("nav",C,[a("ul",null,[a("li",null,[n(o,{to:"#building-a-data-model"},{default:e(()=>[s("Building a Data Model")]),_:1}),a("ul",null,[a("li",null,[n(o,{to:"#properties"},{default:e(()=>[s("Properties")]),_:1})]),a("li",null,[n(o,{to:"#attributes"},{default:e(()=>[s("Attributes")]),_:1})]),a("li",null,[n(o,{to:"#methods"},{default:e(()=>[s("Methods")]),_:1})])])]),a("li",null,[n(o,{to:"#customizing-crud-operations"},{default:e(()=>[s("Customizing CRUD Operations")]),_:1}),a("ul",null,[a("li",null,[n(o,{to:"#data-sources"},{default:e(()=>[s("Data Sources")]),_:1})]),a("li",null,[n(o,{to:"#behaviors"},{default:e(()=>[s("Behaviors")]),_:1})])])]),a("li",null,[n(o,{to:"#standalone-non-ef-entities"},{default:e(()=>[s("Standalone (non-EF) Entities")]),_:1})])])]),m,a("p",null,[s("To start building your data model that Coalesce will generate code for, follow the best practices for "),a("a",h,[s("EF Core"),n(t)]),s(". Guidance on this topic is available in abundance in the "),a("a",v,[s("Entity Framework Core documentation"),n(t)]),s(".")]),b,E,f,a("p",null,[s("Read "),n(l,{to:"/modeling/model-components/properties.html"},{default:e(()=>[s("Properties")]),_:1}),s(" for an outline of the different types of properties that you may place on your models and the code that Coalesce will generate for each of them.")]),g,_,a("p",null,[s("Read "),n(l,{to:"/modeling/model-components/attributes.html"},{default:e(()=>[s("Attributes")]),_:1}),s(" to learn more.")]),F,a("p",null,[s("You can place both static and interface methods on your model classes. Any public methods annotated with "),n(l,{to:"/modeling/model-components/attributes/coalesce.html"},{default:e(()=>[s("[Coalesce]")]),_:1}),s(" will have a generated API endpoint and corresponding generated TypeScript members for calling this API endpoint. Read "),n(l,{to:"/modeling/model-components/methods.html"},{default:e(()=>[s("Methods")]),_:1}),s(" to learn more.")]),B,a("p",null,[s("The method by which you can control what data the users of your application can access through Coalesce's generated APIs is by creating custom data sources. These are classes that allow complete control over the way that data is retrieved from your database and provided to clients. Read "),n(l,{to:"/modeling/model-components/data-sources.html"},{default:e(()=>[s("Data Sources")]),_:1}),s(" to learn more.")]),S,a("p",null,[s("Behaviors in Coalesce are to mutating data as data sources are to reading data. Defining a behaviors class for a model allows complete control over the way that Coalesce will create, update, and delete your application's data in response to requests made through its generated API. Read "),n(l,{to:"/modeling/model-components/behaviors.html"},{default:e(()=>[s("Behaviors")]),_:1}),s(" to learn more.")]),A,x,a("p",null,[s("For these types, you must define at least one custom "),n(l,{to:"/modeling/model-components/data-sources.html"},{default:e(()=>[s("Data Source")]),_:1}),s(", and optionally a "),n(l,{to:"/modeling/model-components/behaviors.html"},{default:e(()=>[s("Behaviors")]),_:1}),s(" class as well. If no behaviors are defined, the type is implicitly read-only, equivalent to turning off create/edit/delete via the "),n(l,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:e(()=>[s("Security Attributes")]),_:1}),s(".")]),w])}const P=c(d,[["render",k],["__file","entities.html.vue"]]);export{P as default}; diff --git a/assets/execute.html.29138de0.js b/assets/execute.html.29138de0.js new file mode 100644 index 000000000..d1e724bb8 --- /dev/null +++ b/assets/execute.html.29138de0.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1e992cc4","path":"/modeling/model-components/attributes/execute.html","title":"[Execute]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1680124405000},"filePathRelative":"modeling/model-components/attributes/execute.md"}');export{e as data}; diff --git a/assets/execute.html.3ada74ae.js b/assets/execute.html.3ada74ae.js new file mode 100644 index 000000000..f923bad3b --- /dev/null +++ b/assets/execute.html.3ada74ae.js @@ -0,0 +1,12 @@ +import{_ as i,y as c,z as p,X as e,B as a,Q as s,$ as l,a5 as r,P as t}from"./framework.fe9a73df.js";const d={},u=e("h1",{id:"execute",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#execute","aria-hidden":"true"},"#"),a(" [Execute]")],-1),D=e("p",null,"Controls permissions for executing of a static or instance method through the API.",-1),h=r(`

Example Usage

public class Person
+{
+    public int PersonId { get; set; }
+    
+    [Coalesce, Execute(Roles = "Payroll,HR")]
+    public void GiveRaise(int centsPerHour) {
+        ...
+    }
+
+    ...
+}
+

Properties

`,3),m=e("p",null,"A comma-separated list of roles which are allowed to execute the method.",-1),y=r("

The level of access to allow for the action for the method.

Enum values are:

  • SecurityPermissionLevels.AllowAll Allow all users to perform the action for the attribute, including users who are not authenticated at all.
  • SecurityPermissionLevels.AllowAuthorized Allow only users who are members of the roles specified on the attribute to perform the action. If no roles are specified on the attribute, then all authenticated users are allowed (no anonymous access).
  • SecurityPermissionLevels.DenyAll Deny the action to all users, regardless of authentication status or authorization level. If DenyAll is used, no API endpoint for the action will be generated.
",3),v=e("p",null,"If true, the method's arguments will be cleared after a successful invocation on admin pages.",-1),f=e("code",null,"CoalesceOptions.ValidateAttributesForMethods",-1),b=e("p",null,"If validation is performed, the method's parameters will be validated by the server and the method invocation prevented if errors are found.",-1);function _(C,g){const o=t("RouterLink"),n=t("Prop");return c(),p("div",null,[u,D,e("p",null,[a("For other security controls, see "),s(o,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:l(()=>[a("Security Attributes")]),_:1}),a(".")]),h,s(n,{def:"public string Roles { get; set; }"}),m,s(n,{def:"public SecurityPermissionLevels PermissionLevel { get; set; } = SecurityPermissionLevels.AllowAuthorized;"}),y,s(n,{def:"public bool AutoClear { get; set; }"}),v,s(n,{def:"public bool? ValidateAttributes { get; set; }"}),e("p",null,[a("If non-null, overrides the value of "),s(o,{to:"/topics/security.html#validateattributesformethods"},{default:l(()=>[f]),_:1}),a(" when determining whether to perform automatic server-side validation of the method's parameters.")]),b])}const A=i(d,[["render",_],["__file","execute.html.vue"]]);export{A as default}; diff --git a/assets/external-types.html.67a4ae13.js b/assets/external-types.html.67a4ae13.js new file mode 100644 index 000000000..eb0284d19 --- /dev/null +++ b/assets/external-types.html.67a4ae13.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3d8f170b","path":"/modeling/model-types/external-types.html","title":"External Types","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Generated Code","slug":"generated-code","link":"#generated-code","children":[]},{"level":2,"title":"Example Data Model","slug":"example-data-model","link":"#example-data-model","children":[]},{"level":2,"title":"Loading & Serialization","slug":"loading-serialization","link":"#loading-serialization","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-types/external-types.md"}');export{e as data}; diff --git a/assets/external-types.html.b5ed5417.js b/assets/external-types.html.b5ed5417.js new file mode 100644 index 000000000..2a32c652f --- /dev/null +++ b/assets/external-types.html.b5ed5417.js @@ -0,0 +1,42 @@ +import{_ as o,y as p,z as t,X as s,B as n,Q as l,$ as e,a5 as D,P as c}from"./framework.fe9a73df.js";const r={},i=s("h1",{id:"external-types",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#external-types","aria-hidden":"true"},"#"),n(" External Types")],-1),y=s("p",null,'In Coalesce, any type which is connected to your data model but is not directly part of it is considered to be an "external type".',-1),d=s("p",null,"The collection of external types for a data model looks like this:",-1),C=s("li",null,"Take all of the property types, method parameters, and method return types of these types.",-1),u=s("li",null,"Any of these types which are not built-in scalar types and not one of the aforementioned api-served types are external types.",-1),h=s("li",null,"For any external type discovered, any of the property types which qualify under the above rules are also external types.",-1),v={class:"custom-container warning"},m=s("p",{class:"custom-container-title"},"WARNING",-1),b=s("p",null,"Be careful when using types that you do not own for properties and method returns in your data model. When Coalesce generates external type ViewModels and DTOs, it will not stop until it has exhausted all paths that can be reached by following public property types and method returns.",-1),g=s("h2",{id:"generated-code",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#generated-code","aria-hidden":"true"},"#"),n(" Generated Code")],-1),E=s("p",null,"For each external type found in your application's model, Coalesce will generate:",-1),_=D(`

Example Data Model

For example, in the following scenario, the following classes are considered as external types:

  • PluginMetadata, exposed through a getter-only property on ApplicationPlugin.
  • PluginResult, exposed through a method return on ApplicationPlugin.

PluginHandler is not because it not exposed by the model, neither directly nor through any of the other external types.

public class AppDbContext : DbContext {
+    public DbSet<Application> Applications { get; set; }
+    public DbSet<ApplicationPlugin> ApplicationPlugins { get; set; }
+}
+
+public class Application {
+    public int ApplicationId { get; set; }
+    public string Name { get; set; }
+    public ICollection<ApplicationPlugin> Plugins { get; set; }
+}
+
+public class ApplicationPlugin {
+    public int ApplicationPluginId { get; set; }
+    public int ApplicationId { get; set; }
+    public Application Application { get; set; }
+
+    public string TypeName { get; set; }
+
+    private PluginHandler GetInstance() => 
+        ((PluginHandler)Activator.CreateInstance(Type.GetType(TypeName)));
+
+    public PluginMetadata Metadata => GetInstance().GetMetadata();
+
+    public PluginResult Invoke(string action, string data) => GetInstance().Invoke(Application, action, data);
+}
+
+public abstract class PluginHandler { 
+    public abstract PluginMetadata GetMetadata();
+    public abstract PluginResult Invoke(Application app, string action, string data);
+}
+
+public abstract class PluginMetadata { 
+    public bool Name { get; set; }
+    public string Version { get; set; }
+    public ICollection<string> Actions { get; set; }
+}
+
+public abstract class PluginResult { 
+    public bool Success { get; set; }
+    public string Message { get; set; }
+}
+

Loading & Serialization

`,6);function f(x,A){const a=c("RouterLink");return p(),t("div",null,[i,y,d,s("ol",null,[s("li",null,[n("Take all of the api-served types in your data model. This includes "),l(a,{to:"/modeling/model-types/entities.html"},{default:e(()=>[n("Entity Models")]),_:1}),n(" and "),l(a,{to:"/modeling/model-types/dtos.html"},{default:e(()=>[n("Custom DTOs")]),_:1}),n(".")]),C,u,h]),s("div",v,[m,b,s("p",null,[n("In general, you should only expose types that you have created so that you will always have full control over them. Mark any properties you don't wish to expose with "),l(a,{to:"/modeling/model-components/attributes/internal-use.html"},{default:e(()=>[n("[InternalUse]")]),_:1}),n(", or make those members non-public.")])]),g,E,s("ul",null,[s("li",null,[n("A "),l(a,{to:"/stacks/agnostic/dtos.html"},{default:e(()=>[n("Generated DTO")]),_:1})]),s("li",null,[n("A "),l(a,{to:"/stacks/disambiguation/external-view-model.html"},{default:e(()=>[n("TypeScript Model")]),_:1})])]),_,s("p",null,[n("External types have slightly different behavior when undergoing serialization to be sent to the client. Unlike database-mapped types which are subject to the rules of "),l(a,{to:"/concepts/include-tree.html"},{default:e(()=>[n("Include Tree")]),_:1}),n(", external types ignore the Include Tree when being mapped to DTOs for serialization. Read "),l(a,{to:"/concepts/include-tree.html#external-type-caveats"},{default:e(()=>[n("External Type Caveats")]),_:1}),n(" for a more detailed explanation of this exception.")])])}const B=o(r,[["render",f],["__file","external-types.html.vue"]]);export{B as default}; diff --git a/assets/external-view-model.html.1df8fd42.js b/assets/external-view-model.html.1df8fd42.js new file mode 100644 index 000000000..7471ef27c --- /dev/null +++ b/assets/external-view-model.html.1df8fd42.js @@ -0,0 +1 @@ +import{_ as s,y as l,z as r,X as t,B as e,Q as o,$ as n,P as i}from"./framework.fe9a73df.js";const d={},c=t("h1",{id:"typescript-external-viewmodels",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#typescript-external-viewmodels","aria-hidden":"true"},"#"),e(" TypeScript External ViewModels")],-1),h=t("p",null,"This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.",-1),p=t("h2",{id:"vue",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#vue","aria-hidden":"true"},"#"),e(" Vue")],-1),u=t("h2",{id:"knockout",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#knockout","aria-hidden":"true"},"#"),e(" Knockout")],-1);function _(m,f){const a=i("RouterLink");return l(),r("div",null,[c,h,p,t("p",null,[e("The Vue stack for Coalesce does not create dedicated ViewModels for "),o(a,{to:"/modeling/model-types/external-types.html"},{default:n(()=>[e("External Types")]),_:1}),e(". The interfaces in the "),o(a,{to:"/stacks/vue/layers/models.html"},{default:n(()=>[e("Model Layer")]),_:1}),e(" are used as the only representation of "),o(a,{to:"/modeling/model-types/external-types.html"},{default:n(()=>[e("External Types")]),_:1}),e(" on the client.")]),u,t("p",null,[e("See: "),o(a,{to:"/stacks/ko/client/external-view-model.html"},{default:n(()=>[e("TypeScript External ViewModels")]),_:1})])])}const y=s(d,[["render",_],["__file","external-view-model.html.vue"]]);export{y as default}; diff --git a/assets/external-view-model.html.280bee2c.js b/assets/external-view-model.html.280bee2c.js new file mode 100644 index 000000000..5430ae35a --- /dev/null +++ b/assets/external-view-model.html.280bee2c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1edd1d5a","path":"/stacks/ko/client/external-view-model.html","title":"TypeScript External ViewModels","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Base Members","slug":"base-members","link":"#base-members","children":[]},{"level":2,"title":"Model-Specific Members","slug":"model-specific-members","link":"#model-specific-members","children":[{"level":3,"title":"Data Properties","slug":"data-properties","link":"#data-properties","children":[]},{"level":3,"title":"Enum Members","slug":"enum-members","link":"#enum-members","children":[]}]}],"git":{"updatedTime":1663268139000},"filePathRelative":"stacks/ko/client/external-view-model.md"}');export{e as data}; diff --git a/assets/external-view-model.html.6ceaf9c7.js b/assets/external-view-model.html.6ceaf9c7.js new file mode 100644 index 000000000..49cc3b71a --- /dev/null +++ b/assets/external-view-model.html.6ceaf9c7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-76e9aad4","path":"/stacks/disambiguation/external-view-model.html","title":"TypeScript External ViewModels","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[]},{"level":2,"title":"Knockout","slug":"knockout","link":"#knockout","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/disambiguation/external-view-model.md"}');export{e as data}; diff --git a/assets/external-view-model.html.c346f050.js b/assets/external-view-model.html.c346f050.js new file mode 100644 index 000000000..e617bb7af --- /dev/null +++ b/assets/external-view-model.html.c346f050.js @@ -0,0 +1,8 @@ +import{_ as d,y as c,z as p,X as e,B as o,Q as t,$ as r,P as l}from"./framework.fe9a73df.js";const u={},b=e("h1",{id:"typescript-external-viewmodels",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typescript-external-viewmodels","aria-hidden":"true"},"#"),o(" TypeScript External ViewModels")],-1),m={href:"https://knockoutjs.com/",target:"_blank",rel:"noopener noreferrer"},h={class:"table-of-contents"},_=e("h2",{id:"base-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#base-members","aria-hidden":"true"},"#"),o(" Base Members")],-1),f=e("h2",{id:"model-specific-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#model-specific-members","aria-hidden":"true"},"#"),o(" Model-Specific Members")],-1),k=e("h3",{id:"data-properties",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#data-properties","aria-hidden":"true"},"#"),o(" Data Properties")],-1),v=e("p",null,[o("For each exposed property on the underlying EF POCO, a "),e("code",null,"KnockoutObservable"),o(" property will exist on the TypeScript model. For navigation properties, these will be typed with the corresponding TypeScript ViewModel for the other end of the relationship. For collections (including collection navigation properties), these properties will be "),e("code",null,"KnockoutObservableArray"),o(" objects.")],-1),y=e("h3",{id:"enum-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#enum-members","aria-hidden":"true"},"#"),o(" Enum Members")],-1),x=e("p",null,[o("For each "),e("code",null,"enum"),o(" property on your POCO, the following will be created:")],-1),w=e("p",null,[o("A "),e("code",null,"KnockoutComputed"),o(" property that will provide the text to display for that property.")],-1);function g(M,K){const a=l("RouterLink"),i=l("ExternalLinkIcon"),n=l("router-link"),s=l("Prop");return c(),p("div",null,[b,e("p",null,[o("For all "),t(a,{to:"/modeling/model-types/external-types.html"},{default:r(()=>[o("External Types")]),_:1}),o(" in your model, Coalesce will generate a TypeScript class that provides a bare-bones representation of that type's properties.")]),e("p",null,[o("These ViewModels are dependent on "),e("a",m,[o("Knockout"),t(i)]),o(", and are designed to be used directly from Knockout bindings in your HTML. All data properties on the generated model are Knockout observables.")]),e("nav",h,[e("ul",null,[e("li",null,[t(n,{to:"#base-members"},{default:r(()=>[o("Base Members")]),_:1})]),e("li",null,[t(n,{to:"#model-specific-members"},{default:r(()=>[o("Model-Specific Members")]),_:1}),e("ul",null,[e("li",null,[t(n,{to:"#data-properties"},{default:r(()=>[o("Data Properties")]),_:1})]),e("li",null,[t(n,{to:"#enum-members"},{default:r(()=>[o("Enum Members")]),_:1})])])])])]),_,e("p",null,[o("The TypeScript ViewModels for external types do not have a common base class, and do not have any of the behaviors or convenience properties that the regular "),t(a,{to:"/stacks/ko/client/view-model.html"},{default:r(()=>[o("TypeScript ViewModels")]),_:1}),o(" for database-mapped classes have.")]),f,k,t(s,{def:` +public personId: KnockoutObservable = ko.observable(null); +public fullName: KnockoutObservable = ko.observable(null); +public gender: KnockoutObservable = ko.observable(null); +public companyId: KnockoutObservable = ko.observable(null); +public company: KnockoutObservable = ko.observable(null); +public addresses: KnockoutObservableArray = ko.observableArray([]); +public birthDate: KnockoutObservable = ko.observable(moment());`,lang:"ts",id:"data-property-members"}),v,y,x,t(s,{def:"public genderText: KnockoutComputed",lang:"ts"}),w])}const O=d(u,[["render",g],["__file","external-view-model.html.vue"]]);export{O as default}; diff --git a/assets/framework.fe9a73df.js b/assets/framework.fe9a73df.js new file mode 100644 index 000000000..e47f3661b --- /dev/null +++ b/assets/framework.fe9a73df.js @@ -0,0 +1,5 @@ +function as(e,t){const n=Object.create(null),s=e.split(",");for(let r=0;r!!n[r.toLowerCase()]:r=>!!n[r]}function ds(e){if(D(e)){const t={};for(let n=0;n{if(n){const s=n.split(Qi);s.length>1&&(t[s[0].trim()]=s[1].trim())}}),t}function hs(e){let t="";if(pe(e))t=e;else if(D(e))for(let n=0;npe(e)?e:e==null?"":D(e)||ue(e)&&(e.toString===Lr||!V(e.toString))?JSON.stringify(e,Ir,2):String(e),Ir=(e,t)=>t&&t.__v_isRef?Ir(e,t.value):wt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[s,r])=>(n[`${s} =>`]=r,n),{})}:Nr(t)?{[`Set(${t.size})`]:[...t.values()]}:ue(t)&&!D(t)&&!kr(t)?String(t):t,ce={},Ct=[],He=()=>{},Gi=()=>!1,eo=/^on[^a-z]/,Gt=e=>eo.test(e),ps=e=>e.startsWith("onUpdate:"),ge=Object.assign,gs=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},to=Object.prototype.hasOwnProperty,te=(e,t)=>to.call(e,t),D=Array.isArray,wt=e=>Pn(e)==="[object Map]",Nr=e=>Pn(e)==="[object Set]",V=e=>typeof e=="function",pe=e=>typeof e=="string",ms=e=>typeof e=="symbol",ue=e=>e!==null&&typeof e=="object",Fr=e=>ue(e)&&V(e.then)&&V(e.catch),Lr=Object.prototype.toString,Pn=e=>Lr.call(e),no=e=>Pn(e).slice(8,-1),kr=e=>Pn(e)==="[object Object]",_s=e=>pe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Dt=as(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),An=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},so=/-(\w)/g,We=An(e=>e.replace(so,(t,n)=>n?n.toUpperCase():"")),ro=/\B([A-Z])/g,_t=An(e=>e.replace(ro,"-$1").toLowerCase()),Tn=An(e=>e.charAt(0).toUpperCase()+e.slice(1)),Hn=An(e=>e?`on${Tn(e)}`:""),zt=(e,t)=>!Object.is(e,t),jn=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},io=e=>{const t=parseFloat(e);return isNaN(t)?e:t},oo=e=>{const t=pe(e)?Number(e):NaN;return isNaN(t)?e:t};let ks;const lo=()=>ks||(ks=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});let Pe;class co{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Pe,!t&&Pe&&(this.index=(Pe.scopes||(Pe.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Pe;try{return Pe=this,t()}finally{Pe=n}}}on(){Pe=this}off(){Pe=this.parent}stop(t){if(this._active){let n,s;for(n=0,s=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},Hr=e=>(e.w&rt)>0,jr=e=>(e.n&rt)>0,fo=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let s=0;s{(f==="length"||f>=c)&&l.push(a)})}else switch(n!==void 0&&l.push(o.get(n)),t){case"add":D(e)?_s(n)&&l.push(o.get("length")):(l.push(o.get(gt)),wt(e)&&l.push(o.get(Zn)));break;case"delete":D(e)||(l.push(o.get(gt)),wt(e)&&l.push(o.get(Zn)));break;case"set":wt(e)&&l.push(o.get(gt));break}if(l.length===1)l[0]&&Gn(l[0]);else{const c=[];for(const a of l)a&&c.push(...a);Gn(ys(c))}}function Gn(e,t){const n=D(e)?e:[...e];for(const s of n)s.computed&&Hs(s);for(const s of n)s.computed||Hs(s)}function Hs(e,t){(e!==ke||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}function ho(e,t){var n;return(n=yn.get(e))===null||n===void 0?void 0:n.get(t)}const po=as("__proto__,__v_isRef,__isVue"),Ur=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ms)),go=bs(),mo=bs(!1,!0),_o=bs(!0),js=yo();function yo(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const s=ne(this);for(let i=0,o=this.length;i{e[t]=function(...n){Ft();const s=ne(this)[t].apply(this,n);return Lt(),s}}),e}function bo(e){const t=ne(this);return xe(t,"has",e),t.hasOwnProperty(e)}function bs(e=!1,t=!1){return function(s,r,i){if(r==="__v_isReactive")return!e;if(r==="__v_isReadonly")return e;if(r==="__v_isShallow")return t;if(r==="__v_raw"&&i===(e?t?Lo:zr:t?Vr:qr).get(s))return s;const o=D(s);if(!e){if(o&&te(js,r))return Reflect.get(js,r,i);if(r==="hasOwnProperty")return bo}const l=Reflect.get(s,r,i);return(ms(r)?Ur.has(r):po(r))||(e||xe(s,"get",r),t)?l:me(l)?o&&_s(r)?l:l.value:ue(l)?e?Qr(l):en(l):l}}const vo=Kr(),Eo=Kr(!0);function Kr(e=!1){return function(n,s,r,i){let o=n[s];if(Tt(o)&&me(o)&&!me(r))return!1;if(!e&&(!bn(r)&&!Tt(r)&&(o=ne(o),r=ne(r)),!D(n)&&me(o)&&!me(r)))return o.value=r,!0;const l=D(n)&&_s(s)?Number(s)e,Sn=e=>Reflect.getPrototypeOf(e);function rn(e,t,n=!1,s=!1){e=e.__v_raw;const r=ne(e),i=ne(t);n||(t!==i&&xe(r,"get",t),xe(r,"get",i));const{has:o}=Sn(r),l=s?vs:n?ws:Qt;if(o.call(r,t))return l(e.get(t));if(o.call(r,i))return l(e.get(i));e!==r&&e.get(t)}function on(e,t=!1){const n=this.__v_raw,s=ne(n),r=ne(e);return t||(e!==r&&xe(s,"has",e),xe(s,"has",r)),e===r?n.has(e):n.has(e)||n.has(r)}function ln(e,t=!1){return e=e.__v_raw,!t&&xe(ne(e),"iterate",gt),Reflect.get(e,"size",e)}function Bs(e){e=ne(e);const t=ne(this);return Sn(t).has.call(t,e)||(t.add(e),ze(t,"add",e,e)),this}function Ds(e,t){t=ne(t);const n=ne(this),{has:s,get:r}=Sn(n);let i=s.call(n,e);i||(e=ne(e),i=s.call(n,e));const o=r.call(n,e);return n.set(e,t),i?zt(t,o)&&ze(n,"set",e,t):ze(n,"add",e,t),this}function Us(e){const t=ne(this),{has:n,get:s}=Sn(t);let r=n.call(t,e);r||(e=ne(e),r=n.call(t,e)),s&&s.call(t,e);const i=t.delete(e);return r&&ze(t,"delete",e,void 0),i}function Ks(){const e=ne(this),t=e.size!==0,n=e.clear();return t&&ze(e,"clear",void 0,void 0),n}function cn(e,t){return function(s,r){const i=this,o=i.__v_raw,l=ne(o),c=t?vs:e?ws:Qt;return!e&&xe(l,"iterate",gt),o.forEach((a,f)=>s.call(r,c(a),c(f),i))}}function un(e,t,n){return function(...s){const r=this.__v_raw,i=ne(r),o=wt(i),l=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,a=r[e](...s),f=n?vs:t?ws:Qt;return!t&&xe(i,"iterate",c?Zn:gt),{next(){const{value:h,done:p}=a.next();return p?{value:h,done:p}:{value:l?[f(h[0]),f(h[1])]:f(h),done:p}},[Symbol.iterator](){return this}}}}function Ye(e){return function(...t){return e==="delete"?!1:this}}function Ao(){const e={get(i){return rn(this,i)},get size(){return ln(this)},has:on,add:Bs,set:Ds,delete:Us,clear:Ks,forEach:cn(!1,!1)},t={get(i){return rn(this,i,!1,!0)},get size(){return ln(this)},has:on,add:Bs,set:Ds,delete:Us,clear:Ks,forEach:cn(!1,!0)},n={get(i){return rn(this,i,!0)},get size(){return ln(this,!0)},has(i){return on.call(this,i,!0)},add:Ye("add"),set:Ye("set"),delete:Ye("delete"),clear:Ye("clear"),forEach:cn(!0,!1)},s={get(i){return rn(this,i,!0,!0)},get size(){return ln(this,!0)},has(i){return on.call(this,i,!0)},add:Ye("add"),set:Ye("set"),delete:Ye("delete"),clear:Ye("clear"),forEach:cn(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(i=>{e[i]=un(i,!1,!1),n[i]=un(i,!0,!1),t[i]=un(i,!1,!0),s[i]=un(i,!0,!0)}),[e,n,t,s]}const[To,Oo,So,Mo]=Ao();function Es(e,t){const n=t?e?Mo:So:e?Oo:To;return(s,r,i)=>r==="__v_isReactive"?!e:r==="__v_isReadonly"?e:r==="__v_raw"?s:Reflect.get(te(n,r)&&r in s?n:s,r,i)}const Io={get:Es(!1,!1)},No={get:Es(!1,!0)},Fo={get:Es(!0,!1)},qr=new WeakMap,Vr=new WeakMap,zr=new WeakMap,Lo=new WeakMap;function ko(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function $o(e){return e.__v_skip||!Object.isExtensible(e)?0:ko(no(e))}function en(e){return Tt(e)?e:Cs(e,!1,Wr,Io,qr)}function Ho(e){return Cs(e,!1,Po,No,Vr)}function Qr(e){return Cs(e,!0,Ro,Fo,zr)}function Cs(e,t,n,s,r){if(!ue(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const i=r.get(e);if(i)return i;const o=$o(e);if(o===0)return e;const l=new Proxy(e,o===2?s:n);return r.set(e,l),l}function xt(e){return Tt(e)?xt(e.__v_raw):!!(e&&e.__v_isReactive)}function Tt(e){return!!(e&&e.__v_isReadonly)}function bn(e){return!!(e&&e.__v_isShallow)}function Yr(e){return xt(e)||Tt(e)}function ne(e){const t=e&&e.__v_raw;return t?ne(t):e}function Jr(e){return _n(e,"__v_skip",!0),e}const Qt=e=>ue(e)?en(e):e,ws=e=>ue(e)?Qr(e):e;function Xr(e){nt&&ke&&(e=ne(e),Dr(e.dep||(e.dep=ys())))}function Zr(e,t){e=ne(e);const n=e.dep;n&&Gn(n)}function me(e){return!!(e&&e.__v_isRef===!0)}function hn(e){return Gr(e,!1)}function jo(e){return Gr(e,!0)}function Gr(e,t){return me(e)?e:new Bo(e,t)}class Bo{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:ne(t),this._value=n?t:Qt(t)}get value(){return Xr(this),this._value}set value(t){const n=this.__v_isShallow||bn(t)||Tt(t);t=n?t:ne(t),zt(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:Qt(t),Zr(this))}}function Rt(e){return me(e)?e.value:e}const Do={get:(e,t,n)=>Rt(Reflect.get(e,t,n)),set:(e,t,n,s)=>{const r=e[t];return me(r)&&!me(n)?(r.value=n,!0):Reflect.set(e,t,n,s)}};function ei(e){return xt(e)?e:new Proxy(e,Do)}function Mu(e){const t=D(e)?new Array(e.length):{};for(const n in e)t[n]=Ko(e,n);return t}class Uo{constructor(t,n,s){this._object=t,this._key=n,this._defaultValue=s,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return ho(ne(this._object),this._key)}}function Ko(e,t,n){const s=e[t];return me(s)?s:new Uo(e,t,n)}var ti;class Wo{constructor(t,n,s,r){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this[ti]=!1,this._dirty=!0,this.effect=new On(t,()=>{this._dirty||(this._dirty=!0,Zr(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!r,this.__v_isReadonly=s}get value(){const t=ne(this);return Xr(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}ti="__v_isReadonly";function qo(e,t,n=!1){let s,r;const i=V(e);return i?(s=e,r=He):(s=e.get,r=e.set),new Wo(s,r,i||!r,n)}function st(e,t,n,s){let r;try{r=s?e(...s):e()}catch(i){tn(i,t,n)}return r}function Me(e,t,n,s){if(V(e)){const i=st(e,t,n,s);return i&&Fr(i)&&i.catch(o=>{tn(o,t,n)}),i}const r=[];for(let i=0;i>>1;Jt(be[s])Ke&&be.splice(t,1)}function Yo(e){D(e)?Pt.push(...e):(!Ve||!Ve.includes(e,e.allowRecurse?at+1:at))&&Pt.push(e),ri()}function Ws(e,t=Yt?Ke+1:0){for(;tJt(n)-Jt(s)),at=0;ate.id==null?1/0:e.id,Jo=(e,t)=>{const n=Jt(e)-Jt(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function ii(e){es=!1,Yt=!0,be.sort(Jo);const t=He;try{for(Ke=0;Kepe(_)?_.trim():_)),h&&(r=n.map(io))}let l,c=s[l=Hn(t)]||s[l=Hn(We(t))];!c&&i&&(c=s[l=Hn(_t(t))]),c&&Me(c,e,6,r);const a=s[l+"Once"];if(a){if(!e.emitted)e.emitted={};else if(e.emitted[l])return;e.emitted[l]=!0,Me(a,e,6,r)}}function oi(e,t,n=!1){const s=t.emitsCache,r=s.get(e);if(r!==void 0)return r;const i=e.emits;let o={},l=!1;if(!V(e)){const c=a=>{const f=oi(a,t,!0);f&&(l=!0,ge(o,f))};!n&&t.mixins.length&&t.mixins.forEach(c),e.extends&&c(e.extends),e.mixins&&e.mixins.forEach(c)}return!i&&!l?(ue(e)&&s.set(e,null),null):(D(i)?i.forEach(c=>o[c]=null):ge(o,i),ue(e)&&s.set(e,o),o)}function In(e,t){return!e||!Gt(t)?!1:(t=t.slice(2).replace(/Once$/,""),te(e,t[0].toLowerCase()+t.slice(1))||te(e,_t(t))||te(e,t))}let _e=null,li=null;function En(e){const t=_e;return _e=e,li=e&&e.type.__scopeId||null,t}function Zo(e,t=_e,n){if(!t||e._n)return e;const s=(...r)=>{s._d&&er(-1);const i=En(t);let o;try{o=e(...r)}finally{En(i),s._d&&er(1)}return o};return s._n=!0,s._c=!0,s._d=!0,s}function Bn(e){const{type:t,vnode:n,proxy:s,withProxy:r,props:i,propsOptions:[o],slots:l,attrs:c,emit:a,render:f,renderCache:h,data:p,setupState:_,ctx:C,inheritAttrs:A}=e;let k,g;const y=En(e);try{if(n.shapeFlag&4){const $=r||s;k=Le(f.call($,$,h,i,_,p,C)),g=c}else{const $=t;k=Le($.length>1?$(i,{attrs:c,slots:l,emit:a}):$(i,null)),g=t.props?c:Go(c)}}catch($){Wt.length=0,tn($,e,1),k=de(Ae)}let P=k;if(g&&A!==!1){const $=Object.keys(g),{shapeFlag:U}=P;$.length&&U&7&&(o&&$.some(ps)&&(g=el(g,o)),P=it(P,g))}return n.dirs&&(P=it(P),P.dirs=P.dirs?P.dirs.concat(n.dirs):n.dirs),n.transition&&(P.transition=n.transition),k=P,En(y),k}const Go=e=>{let t;for(const n in e)(n==="class"||n==="style"||Gt(n))&&((t||(t={}))[n]=e[n]);return t},el=(e,t)=>{const n={};for(const s in e)(!ps(s)||!(s.slice(9)in t))&&(n[s]=e[s]);return n};function tl(e,t,n){const{props:s,children:r,component:i}=e,{props:o,children:l,patchFlag:c}=t,a=i.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&c>=0){if(c&1024)return!0;if(c&16)return s?qs(s,o,a):!!o;if(c&8){const f=t.dynamicProps;for(let h=0;he.__isSuspense;function ci(e,t){t&&t.pendingBranch?D(e)?t.effects.push(...e):t.effects.push(e):Yo(e)}function pn(e,t){if(ae){let n=ae.provides;const s=ae.parent&&ae.parent.provides;s===n&&(n=ae.provides=Object.create(s)),n[e]=t}}function je(e,t,n=!1){const s=ae||_e;if(s){const r=s.parent==null?s.vnode.appContext&&s.vnode.appContext.provides:s.parent.provides;if(r&&e in r)return r[e];if(arguments.length>1)return n&&V(t)?t.call(s.proxy):t}}function Iu(e,t){return Rs(e,null,t)}const fn={};function gn(e,t,n){return Rs(e,t,n)}function Rs(e,t,{immediate:n,deep:s,flush:r,onTrack:i,onTrigger:o}=ce){const l=uo()===(ae==null?void 0:ae.scope)?ae:null;let c,a=!1,f=!1;if(me(e)?(c=()=>e.value,a=bn(e)):xt(e)?(c=()=>e,s=!0):D(e)?(f=!0,a=e.some(P=>xt(P)||bn(P)),c=()=>e.map(P=>{if(me(P))return P.value;if(xt(P))return pt(P);if(V(P))return st(P,l,2)})):V(e)?t?c=()=>st(e,l,2):c=()=>{if(!(l&&l.isUnmounted))return h&&h(),Me(e,l,3,[p])}:c=He,t&&s){const P=c;c=()=>pt(P())}let h,p=P=>{h=g.onStop=()=>{st(P,l,4)}},_;if(Mt)if(p=He,t?n&&Me(t,l,3,[c(),f?[]:void 0,p]):c(),r==="sync"){const P=Zl();_=P.__watcherHandles||(P.__watcherHandles=[])}else return He;let C=f?new Array(e.length).fill(fn):fn;const A=()=>{if(!!g.active)if(t){const P=g.run();(s||a||(f?P.some(($,U)=>zt($,C[U])):zt(P,C)))&&(h&&h(),Me(t,l,3,[P,C===fn?void 0:f&&C[0]===fn?[]:C,p]),C=P)}else g.run()};A.allowRecurse=!!t;let k;r==="sync"?k=A:r==="post"?k=()=>Ce(A,l&&l.suspense):(A.pre=!0,l&&(A.id=l.uid),k=()=>Mn(A));const g=new On(c,k);t?n?A():C=g.run():r==="post"?Ce(g.run.bind(g),l&&l.suspense):g.run();const y=()=>{g.stop(),l&&l.scope&&gs(l.scope.effects,g)};return _&&_.push(y),y}function rl(e,t,n){const s=this.proxy,r=pe(e)?e.includes(".")?ui(s,e):()=>s[e]:e.bind(s,s);let i;V(t)?i=t:(i=t.handler,n=t);const o=ae;St(this);const l=Rs(r,i.bind(s),n);return o?St(o):mt(),l}function ui(e,t){const n=t.split(".");return()=>{let s=e;for(let r=0;r{pt(n,t)});else if(kr(e))for(const n in e)pt(e[n],t);return e}function il(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return pi(()=>{e.isMounted=!0}),gi(()=>{e.isUnmounting=!0}),e}const Te=[Function,Array],ol={name:"BaseTransition",props:{mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Te,onEnter:Te,onAfterEnter:Te,onEnterCancelled:Te,onBeforeLeave:Te,onLeave:Te,onAfterLeave:Te,onLeaveCancelled:Te,onBeforeAppear:Te,onAppear:Te,onAfterAppear:Te,onAppearCancelled:Te},setup(e,{slots:t}){const n=Wl(),s=il();let r;return()=>{const i=t.default&&di(t.default(),!0);if(!i||!i.length)return;let o=i[0];if(i.length>1){for(const A of i)if(A.type!==Ae){o=A;break}}const l=ne(e),{mode:c}=l;if(s.isLeaving)return Dn(o);const a=Vs(o);if(!a)return Dn(o);const f=ts(a,l,s,n);ns(a,f);const h=n.subTree,p=h&&Vs(h);let _=!1;const{getTransitionKey:C}=a.type;if(C){const A=C();r===void 0?r=A:A!==r&&(r=A,_=!0)}if(p&&p.type!==Ae&&(!dt(a,p)||_)){const A=ts(p,l,s,n);if(ns(p,A),c==="out-in")return s.isLeaving=!0,A.afterLeave=()=>{s.isLeaving=!1,n.update.active!==!1&&n.update()},Dn(o);c==="in-out"&&a.type!==Ae&&(A.delayLeave=(k,g,y)=>{const P=ai(s,p);P[String(p.key)]=p,k._leaveCb=()=>{g(),k._leaveCb=void 0,delete f.delayedLeave},f.delayedLeave=y})}return o}}},fi=ol;function ai(e,t){const{leavingVNodes:n}=e;let s=n.get(t.type);return s||(s=Object.create(null),n.set(t.type,s)),s}function ts(e,t,n,s){const{appear:r,mode:i,persisted:o=!1,onBeforeEnter:l,onEnter:c,onAfterEnter:a,onEnterCancelled:f,onBeforeLeave:h,onLeave:p,onAfterLeave:_,onLeaveCancelled:C,onBeforeAppear:A,onAppear:k,onAfterAppear:g,onAppearCancelled:y}=t,P=String(e.key),$=ai(n,e),U=(S,q)=>{S&&Me(S,s,9,q)},J=(S,q)=>{const K=q[1];U(S,q),D(S)?S.every(Z=>Z.length<=1)&&K():S.length<=1&&K()},z={mode:i,persisted:o,beforeEnter(S){let q=l;if(!n.isMounted)if(r)q=A||l;else return;S._leaveCb&&S._leaveCb(!0);const K=$[P];K&&dt(e,K)&&K.el._leaveCb&&K.el._leaveCb(),U(q,[S])},enter(S){let q=c,K=a,Z=f;if(!n.isMounted)if(r)q=k||c,K=g||a,Z=y||f;else return;let N=!1;const Q=S._enterCb=L=>{N||(N=!0,L?U(Z,[S]):U(K,[S]),z.delayedLeave&&z.delayedLeave(),S._enterCb=void 0)};q?J(q,[S,Q]):Q()},leave(S,q){const K=String(e.key);if(S._enterCb&&S._enterCb(!0),n.isUnmounting)return q();U(h,[S]);let Z=!1;const N=S._leaveCb=Q=>{Z||(Z=!0,q(),Q?U(C,[S]):U(_,[S]),S._leaveCb=void 0,$[K]===e&&delete $[K])};$[K]=e,p?J(p,[S,N]):N()},clone(S){return ts(S,t,n,s)}};return z}function Dn(e){if(nn(e))return e=it(e),e.children=null,e}function Vs(e){return nn(e)?e.children?e.children[0]:void 0:e}function ns(e,t){e.shapeFlag&6&&e.component?ns(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function di(e,t=!1,n){let s=[],r=0;for(let i=0;i1)for(let i=0;i!!e.type.__asyncLoader;function Nu(e){V(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:s,delay:r=200,timeout:i,suspensible:o=!0,onError:l}=e;let c=null,a,f=0;const h=()=>(f++,c=null,p()),p=()=>{let _;return c||(_=c=t().catch(C=>{if(C=C instanceof Error?C:new Error(String(C)),l)return new Promise((A,k)=>{l(C,()=>A(h()),()=>k(C),f+1)});throw C}).then(C=>_!==c&&c?c:(C&&(C.__esModule||C[Symbol.toStringTag]==="Module")&&(C=C.default),a=C,C)))};return Ps({name:"AsyncComponentWrapper",__asyncLoader:p,get __asyncResolved(){return a},setup(){const _=ae;if(a)return()=>Un(a,_);const C=y=>{c=null,tn(y,_,13,!s)};if(o&&_.suspense||Mt)return p().then(y=>()=>Un(y,_)).catch(y=>(C(y),()=>s?de(s,{error:y}):null));const A=hn(!1),k=hn(),g=hn(!!r);return r&&setTimeout(()=>{g.value=!1},r),i!=null&&setTimeout(()=>{if(!A.value&&!k.value){const y=new Error(`Async component timed out after ${i}ms.`);C(y),k.value=y}},i),p().then(()=>{A.value=!0,_.parent&&nn(_.parent.vnode)&&Mn(_.parent.update)}).catch(y=>{C(y),k.value=y}),()=>{if(A.value&&a)return Un(a,_);if(k.value&&s)return de(s,{error:k.value});if(n&&!g.value)return de(n)}}})}function Un(e,t){const{ref:n,props:s,children:r,ce:i}=t.vnode,o=de(e,s,r);return o.ref=n,o.ce=i,delete t.vnode.ce,o}const nn=e=>e.type.__isKeepAlive;function ll(e,t){hi(e,"a",t)}function cl(e,t){hi(e,"da",t)}function hi(e,t,n=ae){const s=e.__wdc||(e.__wdc=()=>{let r=n;for(;r;){if(r.isDeactivated)return;r=r.parent}return e()});if(Nn(t,s,n),n){let r=n.parent;for(;r&&r.parent;)nn(r.parent.vnode)&&ul(s,t,n,r),r=r.parent}}function ul(e,t,n,s){const r=Nn(t,e,s,!0);mi(()=>{gs(s[t],r)},n)}function Nn(e,t,n=ae,s=!1){if(n){const r=n[e]||(n[e]=[]),i=t.__weh||(t.__weh=(...o)=>{if(n.isUnmounted)return;Ft(),St(n);const l=Me(t,n,e,o);return mt(),Lt(),l});return s?r.unshift(i):r.push(i),i}}const Qe=e=>(t,n=ae)=>(!Mt||e==="sp")&&Nn(e,(...s)=>t(...s),n),fl=Qe("bm"),pi=Qe("m"),al=Qe("bu"),dl=Qe("u"),gi=Qe("bum"),mi=Qe("um"),hl=Qe("sp"),pl=Qe("rtg"),gl=Qe("rtc");function ml(e,t=ae){Nn("ec",e,t)}function Fu(e,t){const n=_e;if(n===null)return e;const s=Ln(n)||n.proxy,r=e.dirs||(e.dirs=[]);for(let i=0;it(o,l,void 0,i&&i[l]));else{const o=Object.keys(e);r=new Array(o.length);for(let l=0,c=o.length;lxn(t)?!(t.type===Ae||t.type===we&&!yi(t.children)):!0)?e:null}const ss=e=>e?Ni(e)?Ln(e)||e.proxy:ss(e.parent):null,Ut=ge(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>ss(e.parent),$root:e=>ss(e.root),$emit:e=>e.emit,$options:e=>As(e),$forceUpdate:e=>e.f||(e.f=()=>Mn(e.update)),$nextTick:e=>e.n||(e.n=si.bind(e.proxy)),$watch:e=>rl.bind(e)}),Kn=(e,t)=>e!==ce&&!e.__isScriptSetup&&te(e,t),bl={get({_:e},t){const{ctx:n,setupState:s,data:r,props:i,accessCache:o,type:l,appContext:c}=e;let a;if(t[0]!=="$"){const _=o[t];if(_!==void 0)switch(_){case 1:return s[t];case 2:return r[t];case 4:return n[t];case 3:return i[t]}else{if(Kn(s,t))return o[t]=1,s[t];if(r!==ce&&te(r,t))return o[t]=2,r[t];if((a=e.propsOptions[0])&&te(a,t))return o[t]=3,i[t];if(n!==ce&&te(n,t))return o[t]=4,n[t];rs&&(o[t]=0)}}const f=Ut[t];let h,p;if(f)return t==="$attrs"&&xe(e,"get",t),f(e);if((h=l.__cssModules)&&(h=h[t]))return h;if(n!==ce&&te(n,t))return o[t]=4,n[t];if(p=c.config.globalProperties,te(p,t))return p[t]},set({_:e},t,n){const{data:s,setupState:r,ctx:i}=e;return Kn(r,t)?(r[t]=n,!0):s!==ce&&te(s,t)?(s[t]=n,!0):te(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(i[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:s,appContext:r,propsOptions:i}},o){let l;return!!n[o]||e!==ce&&te(e,o)||Kn(t,o)||(l=i[0])&&te(l,o)||te(s,o)||te(Ut,o)||te(r.config.globalProperties,o)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:te(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};let rs=!0;function vl(e){const t=As(e),n=e.proxy,s=e.ctx;rs=!1,t.beforeCreate&&Qs(t.beforeCreate,e,"bc");const{data:r,computed:i,methods:o,watch:l,provide:c,inject:a,created:f,beforeMount:h,mounted:p,beforeUpdate:_,updated:C,activated:A,deactivated:k,beforeDestroy:g,beforeUnmount:y,destroyed:P,unmounted:$,render:U,renderTracked:J,renderTriggered:z,errorCaptured:S,serverPrefetch:q,expose:K,inheritAttrs:Z,components:N,directives:Q,filters:L}=t;if(a&&El(a,s,null,e.appContext.config.unwrapInjectedRef),o)for(const oe in o){const re=o[oe];V(re)&&(s[oe]=re.bind(n))}if(r){const oe=r.call(n,n);ue(oe)&&(e.data=en(oe))}if(rs=!0,i)for(const oe in i){const re=i[oe],Ie=V(re)?re.bind(n,n):V(re.get)?re.get.bind(n,n):He,ot=!V(re)&&V(re.set)?re.set.bind(n):He,Ne=Se({get:Ie,set:ot});Object.defineProperty(s,oe,{enumerable:!0,configurable:!0,get:()=>Ne.value,set:Ee=>Ne.value=Ee})}if(l)for(const oe in l)bi(l[oe],s,n,oe);if(c){const oe=V(c)?c.call(n):c;Reflect.ownKeys(oe).forEach(re=>{pn(re,oe[re])})}f&&Qs(f,e,"c");function G(oe,re){D(re)?re.forEach(Ie=>oe(Ie.bind(n))):re&&oe(re.bind(n))}if(G(fl,h),G(pi,p),G(al,_),G(dl,C),G(ll,A),G(cl,k),G(ml,S),G(gl,J),G(pl,z),G(gi,y),G(mi,$),G(hl,q),D(K))if(K.length){const oe=e.exposed||(e.exposed={});K.forEach(re=>{Object.defineProperty(oe,re,{get:()=>n[re],set:Ie=>n[re]=Ie})})}else e.exposed||(e.exposed={});U&&e.render===He&&(e.render=U),Z!=null&&(e.inheritAttrs=Z),N&&(e.components=N),Q&&(e.directives=Q)}function El(e,t,n=He,s=!1){D(e)&&(e=is(e));for(const r in e){const i=e[r];let o;ue(i)?"default"in i?o=je(i.from||r,i.default,!0):o=je(i.from||r):o=je(i),me(o)&&s?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>o.value,set:l=>o.value=l}):t[r]=o}}function Qs(e,t,n){Me(D(e)?e.map(s=>s.bind(t.proxy)):e.bind(t.proxy),t,n)}function bi(e,t,n,s){const r=s.includes(".")?ui(n,s):()=>n[s];if(pe(e)){const i=t[e];V(i)&&gn(r,i)}else if(V(e))gn(r,e.bind(n));else if(ue(e))if(D(e))e.forEach(i=>bi(i,t,n,s));else{const i=V(e.handler)?e.handler.bind(n):t[e.handler];V(i)&&gn(r,i,e)}}function As(e){const t=e.type,{mixins:n,extends:s}=t,{mixins:r,optionsCache:i,config:{optionMergeStrategies:o}}=e.appContext,l=i.get(t);let c;return l?c=l:!r.length&&!n&&!s?c=t:(c={},r.length&&r.forEach(a=>Cn(c,a,o,!0)),Cn(c,t,o)),ue(t)&&i.set(t,c),c}function Cn(e,t,n,s=!1){const{mixins:r,extends:i}=t;i&&Cn(e,i,n,!0),r&&r.forEach(o=>Cn(e,o,n,!0));for(const o in t)if(!(s&&o==="expose")){const l=Cl[o]||n&&n[o];e[o]=l?l(e[o],t[o]):t[o]}return e}const Cl={data:Ys,props:ft,emits:ft,methods:ft,computed:ft,beforeCreate:ve,created:ve,beforeMount:ve,mounted:ve,beforeUpdate:ve,updated:ve,beforeDestroy:ve,beforeUnmount:ve,destroyed:ve,unmounted:ve,activated:ve,deactivated:ve,errorCaptured:ve,serverPrefetch:ve,components:ft,directives:ft,watch:xl,provide:Ys,inject:wl};function Ys(e,t){return t?e?function(){return ge(V(e)?e.call(this,this):e,V(t)?t.call(this,this):t)}:t:e}function wl(e,t){return ft(is(e),is(t))}function is(e){if(D(e)){const t={};for(let n=0;n0)&&!(o&16)){if(o&8){const f=e.vnode.dynamicProps;for(let h=0;h{c=!0;const[p,_]=Ei(h,t,!0);ge(o,p),_&&l.push(..._)};!n&&t.mixins.length&&t.mixins.forEach(f),e.extends&&f(e.extends),e.mixins&&e.mixins.forEach(f)}if(!i&&!c)return ue(e)&&s.set(e,Ct),Ct;if(D(i))for(let f=0;f-1,_[1]=A<0||C-1||te(_,"default"))&&l.push(h)}}}const a=[o,l];return ue(e)&&s.set(e,a),a}function Js(e){return e[0]!=="$"}function Xs(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function Zs(e,t){return Xs(e)===Xs(t)}function Gs(e,t){return D(t)?t.findIndex(n=>Zs(n,e)):V(t)&&Zs(t,e)?0:-1}const Ci=e=>e[0]==="_"||e==="$stable",Ts=e=>D(e)?e.map(Le):[Le(e)],Al=(e,t,n)=>{if(t._n)return t;const s=Zo((...r)=>Ts(t(...r)),n);return s._c=!1,s},wi=(e,t,n)=>{const s=e._ctx;for(const r in e){if(Ci(r))continue;const i=e[r];if(V(i))t[r]=Al(r,i,s);else if(i!=null){const o=Ts(i);t[r]=()=>o}}},xi=(e,t)=>{const n=Ts(t);e.slots.default=()=>n},Tl=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=ne(t),_n(t,"_",n)):wi(t,e.slots={})}else e.slots={},t&&xi(e,t);_n(e.slots,Fn,1)},Ol=(e,t,n)=>{const{vnode:s,slots:r}=e;let i=!0,o=ce;if(s.shapeFlag&32){const l=t._;l?n&&l===1?i=!1:(ge(r,t),!n&&l===1&&delete r._):(i=!t.$stable,wi(t,r)),o=t}else t&&(xi(e,t),o={default:1});if(i)for(const l in r)!Ci(l)&&!(l in o)&&delete r[l]};function Ri(){return{app:null,config:{isNativeTag:Gi,performance:!1,globalProperties:{},optionMergeStrategies:{},errorHandler:void 0,warnHandler:void 0,compilerOptions:{}},mixins:[],components:{},directives:{},provides:Object.create(null),optionsCache:new WeakMap,propsCache:new WeakMap,emitsCache:new WeakMap}}let Sl=0;function Ml(e,t){return function(s,r=null){V(s)||(s=Object.assign({},s)),r!=null&&!ue(r)&&(r=null);const i=Ri(),o=new Set;let l=!1;const c=i.app={_uid:Sl++,_component:s,_props:r,_container:null,_context:i,_instance:null,version:Gl,get config(){return i.config},set config(a){},use(a,...f){return o.has(a)||(a&&V(a.install)?(o.add(a),a.install(c,...f)):V(a)&&(o.add(a),a(c,...f))),c},mixin(a){return i.mixins.includes(a)||i.mixins.push(a),c},component(a,f){return f?(i.components[a]=f,c):i.components[a]},directive(a,f){return f?(i.directives[a]=f,c):i.directives[a]},mount(a,f,h){if(!l){const p=de(s,r);return p.appContext=i,f&&t?t(p,a):e(p,a,h),l=!0,c._container=a,a.__vue_app__=c,Ln(p.component)||p.component.proxy}},unmount(){l&&(e(null,c._container),delete c._container.__vue_app__)},provide(a,f){return i.provides[a]=f,c}};return c}}function wn(e,t,n,s,r=!1){if(D(e)){e.forEach((p,_)=>wn(p,t&&(D(t)?t[_]:t),n,s,r));return}if(At(s)&&!r)return;const i=s.shapeFlag&4?Ln(s.component)||s.component.proxy:s.el,o=r?null:i,{i:l,r:c}=e,a=t&&t.r,f=l.refs===ce?l.refs={}:l.refs,h=l.setupState;if(a!=null&&a!==c&&(pe(a)?(f[a]=null,te(h,a)&&(h[a]=null)):me(a)&&(a.value=null)),V(c))st(c,l,12,[o,f]);else{const p=pe(c),_=me(c);if(p||_){const C=()=>{if(e.f){const A=p?te(h,c)?h[c]:f[c]:c.value;r?D(A)&&gs(A,i):D(A)?A.includes(i)||A.push(i):p?(f[c]=[i],te(h,c)&&(h[c]=f[c])):(c.value=[i],e.k&&(f[e.k]=c.value))}else p?(f[c]=o,te(h,c)&&(h[c]=o)):_&&(c.value=o,e.k&&(f[e.k]=o))};o?(C.id=-1,Ce(C,n)):C()}}}let Je=!1;const an=e=>/svg/.test(e.namespaceURI)&&e.tagName!=="foreignObject",dn=e=>e.nodeType===8;function Il(e){const{mt:t,p:n,o:{patchProp:s,createText:r,nextSibling:i,parentNode:o,remove:l,insert:c,createComment:a}}=e,f=(g,y)=>{if(!y.hasChildNodes()){n(null,g,y),vn(),y._vnode=g;return}Je=!1,h(y.firstChild,g,null,null,null),vn(),y._vnode=g,Je&&console.error("Hydration completed but contains mismatches.")},h=(g,y,P,$,U,J=!1)=>{const z=dn(g)&&g.data==="[",S=()=>A(g,y,P,$,U,z),{type:q,ref:K,shapeFlag:Z,patchFlag:N}=y;let Q=g.nodeType;y.el=g,N===-2&&(J=!1,y.dynamicChildren=null);let L=null;switch(q){case Ot:Q!==3?y.children===""?(c(y.el=r(""),o(g),g),L=g):L=S():(g.data!==y.children&&(Je=!0,g.data=y.children),L=i(g));break;case Ae:Q!==8||z?L=S():L=i(g);break;case Kt:if(z&&(g=i(g),Q=g.nodeType),Q===1||Q===3){L=g;const ye=!y.children.length;for(let G=0;G{J=J||!!y.dynamicChildren;const{type:z,props:S,patchFlag:q,shapeFlag:K,dirs:Z}=y,N=z==="input"&&Z||z==="option";if(N||q!==-1){if(Z&&Ue(y,null,P,"created"),S)if(N||!J||q&48)for(const L in S)(N&&L.endsWith("value")||Gt(L)&&!Dt(L))&&s(g,L,null,S[L],!1,void 0,P);else S.onClick&&s(g,"onClick",null,S.onClick,!1,void 0,P);let Q;if((Q=S&&S.onVnodeBeforeMount)&&Oe(Q,P,y),Z&&Ue(y,null,P,"beforeMount"),((Q=S&&S.onVnodeMounted)||Z)&&ci(()=>{Q&&Oe(Q,P,y),Z&&Ue(y,null,P,"mounted")},$),K&16&&!(S&&(S.innerHTML||S.textContent))){let L=_(g.firstChild,y,g,P,$,U,J);for(;L;){Je=!0;const ye=L;L=L.nextSibling,l(ye)}}else K&8&&g.textContent!==y.children&&(Je=!0,g.textContent=y.children)}return g.nextSibling},_=(g,y,P,$,U,J,z)=>{z=z||!!y.dynamicChildren;const S=y.children,q=S.length;for(let K=0;K{const{slotScopeIds:z}=y;z&&(U=U?U.concat(z):z);const S=o(g),q=_(i(g),y,S,P,$,U,J);return q&&dn(q)&&q.data==="]"?i(y.anchor=q):(Je=!0,c(y.anchor=a("]"),S,q),q)},A=(g,y,P,$,U,J)=>{if(Je=!0,y.el=null,J){const q=k(g);for(;;){const K=i(g);if(K&&K!==q)l(K);else break}}const z=i(g),S=o(g);return l(g),n(null,y,S,z,P,$,an(S),U),z},k=g=>{let y=0;for(;g;)if(g=i(g),g&&dn(g)&&(g.data==="["&&y++,g.data==="]")){if(y===0)return i(g);y--}return g};return[f,h]}const Ce=ci;function Nl(e){return Fl(e,Il)}function Fl(e,t){const n=lo();n.__VUE__=!0;const{insert:s,remove:r,patchProp:i,createElement:o,createText:l,createComment:c,setText:a,setElementText:f,parentNode:h,nextSibling:p,setScopeId:_=He,insertStaticContent:C}=e,A=(u,d,m,b=null,E=null,R=null,M=!1,x=null,T=!!d.dynamicChildren)=>{if(u===d)return;u&&!dt(u,d)&&(b=O(u),Ee(u,E,R,!0),u=null),d.patchFlag===-2&&(T=!1,d.dynamicChildren=null);const{type:w,ref:j,shapeFlag:F}=d;switch(w){case Ot:k(u,d,m,b);break;case Ae:g(u,d,m,b);break;case Kt:u==null&&y(d,m,b,M);break;case we:N(u,d,m,b,E,R,M,x,T);break;default:F&1?U(u,d,m,b,E,R,M,x,T):F&6?Q(u,d,m,b,E,R,M,x,T):(F&64||F&128)&&w.process(u,d,m,b,E,R,M,x,T,ee)}j!=null&&E&&wn(j,u&&u.ref,R,d||u,!d)},k=(u,d,m,b)=>{if(u==null)s(d.el=l(d.children),m,b);else{const E=d.el=u.el;d.children!==u.children&&a(E,d.children)}},g=(u,d,m,b)=>{u==null?s(d.el=c(d.children||""),m,b):d.el=u.el},y=(u,d,m,b)=>{[u.el,u.anchor]=C(u.children,d,m,b,u.el,u.anchor)},P=({el:u,anchor:d},m,b)=>{let E;for(;u&&u!==d;)E=p(u),s(u,m,b),u=E;s(d,m,b)},$=({el:u,anchor:d})=>{let m;for(;u&&u!==d;)m=p(u),r(u),u=m;r(d)},U=(u,d,m,b,E,R,M,x,T)=>{M=M||d.type==="svg",u==null?J(d,m,b,E,R,M,x,T):q(u,d,E,R,M,x,T)},J=(u,d,m,b,E,R,M,x)=>{let T,w;const{type:j,props:F,shapeFlag:B,transition:W,dirs:X}=u;if(T=u.el=o(u.type,R,F&&F.is,F),B&8?f(T,u.children):B&16&&S(u.children,T,null,b,E,R&&j!=="foreignObject",M,x),X&&Ue(u,null,b,"created"),z(T,u,u.scopeId,M,b),F){for(const ie in F)ie!=="value"&&!Dt(ie)&&i(T,ie,null,F[ie],R,u.children,b,E,I);"value"in F&&i(T,"value",null,F.value),(w=F.onVnodeBeforeMount)&&Oe(w,b,u)}X&&Ue(u,null,b,"beforeMount");const le=(!E||E&&!E.pendingBranch)&&W&&!W.persisted;le&&W.beforeEnter(T),s(T,d,m),((w=F&&F.onVnodeMounted)||le||X)&&Ce(()=>{w&&Oe(w,b,u),le&&W.enter(T),X&&Ue(u,null,b,"mounted")},E)},z=(u,d,m,b,E)=>{if(m&&_(u,m),b)for(let R=0;R{for(let w=T;w{const x=d.el=u.el;let{patchFlag:T,dynamicChildren:w,dirs:j}=d;T|=u.patchFlag&16;const F=u.props||ce,B=d.props||ce;let W;m&<(m,!1),(W=B.onVnodeBeforeUpdate)&&Oe(W,m,d,u),j&&Ue(d,u,m,"beforeUpdate"),m&<(m,!0);const X=E&&d.type!=="foreignObject";if(w?K(u.dynamicChildren,w,x,m,b,X,R):M||re(u,d,x,null,m,b,X,R,!1),T>0){if(T&16)Z(x,d,F,B,m,b,E);else if(T&2&&F.class!==B.class&&i(x,"class",null,B.class,E),T&4&&i(x,"style",F.style,B.style,E),T&8){const le=d.dynamicProps;for(let ie=0;ie{W&&Oe(W,m,d,u),j&&Ue(d,u,m,"updated")},b)},K=(u,d,m,b,E,R,M)=>{for(let x=0;x{if(m!==b){if(m!==ce)for(const x in m)!Dt(x)&&!(x in b)&&i(u,x,m[x],null,M,d.children,E,R,I);for(const x in b){if(Dt(x))continue;const T=b[x],w=m[x];T!==w&&x!=="value"&&i(u,x,w,T,M,d.children,E,R,I)}"value"in b&&i(u,"value",m.value,b.value)}},N=(u,d,m,b,E,R,M,x,T)=>{const w=d.el=u?u.el:l(""),j=d.anchor=u?u.anchor:l("");let{patchFlag:F,dynamicChildren:B,slotScopeIds:W}=d;W&&(x=x?x.concat(W):W),u==null?(s(w,m,b),s(j,m,b),S(d.children,m,j,E,R,M,x,T)):F>0&&F&64&&B&&u.dynamicChildren?(K(u.dynamicChildren,B,m,E,R,M,x),(d.key!=null||E&&d===E.subTree)&&Pi(u,d,!0)):re(u,d,m,j,E,R,M,x,T)},Q=(u,d,m,b,E,R,M,x,T)=>{d.slotScopeIds=x,u==null?d.shapeFlag&512?E.ctx.activate(d,m,b,M,T):L(d,m,b,E,R,M,T):ye(u,d,T)},L=(u,d,m,b,E,R,M)=>{const x=u.component=Kl(u,b,E);if(nn(u)&&(x.ctx.renderer=ee),ql(x),x.asyncDep){if(E&&E.registerDep(x,G),!u.el){const T=x.subTree=de(Ae);g(null,T,d,m)}return}G(x,u,d,m,E,R,M)},ye=(u,d,m)=>{const b=d.component=u.component;if(tl(u,d,m))if(b.asyncDep&&!b.asyncResolved){oe(b,d,m);return}else b.next=d,Qo(b.update),b.update();else d.el=u.el,b.vnode=d},G=(u,d,m,b,E,R,M)=>{const x=()=>{if(u.isMounted){let{next:j,bu:F,u:B,parent:W,vnode:X}=u,le=j,ie;lt(u,!1),j?(j.el=X.el,oe(u,j,M)):j=X,F&&jn(F),(ie=j.props&&j.props.onVnodeBeforeUpdate)&&Oe(ie,W,j,X),lt(u,!0);const he=Bn(u),Fe=u.subTree;u.subTree=he,A(Fe,he,h(Fe.el),O(Fe),u,E,R),j.el=he.el,le===null&&nl(u,he.el),B&&Ce(B,E),(ie=j.props&&j.props.onVnodeUpdated)&&Ce(()=>Oe(ie,W,j,X),E)}else{let j;const{el:F,props:B}=d,{bm:W,m:X,parent:le}=u,ie=At(d);if(lt(u,!1),W&&jn(W),!ie&&(j=B&&B.onVnodeBeforeMount)&&Oe(j,le,d),lt(u,!0),F&&Y){const he=()=>{u.subTree=Bn(u),Y(F,u.subTree,u,E,null)};ie?d.type.__asyncLoader().then(()=>!u.isUnmounted&&he()):he()}else{const he=u.subTree=Bn(u);A(null,he,m,b,u,E,R),d.el=he.el}if(X&&Ce(X,E),!ie&&(j=B&&B.onVnodeMounted)){const he=d;Ce(()=>Oe(j,le,he),E)}(d.shapeFlag&256||le&&At(le.vnode)&&le.vnode.shapeFlag&256)&&u.a&&Ce(u.a,E),u.isMounted=!0,d=m=b=null}},T=u.effect=new On(x,()=>Mn(w),u.scope),w=u.update=()=>T.run();w.id=u.uid,lt(u,!0),w()},oe=(u,d,m)=>{d.component=u;const b=u.vnode.props;u.vnode=d,u.next=null,Pl(u,d.props,b,m),Ol(u,d.children,m),Ft(),Ws(),Lt()},re=(u,d,m,b,E,R,M,x,T=!1)=>{const w=u&&u.children,j=u?u.shapeFlag:0,F=d.children,{patchFlag:B,shapeFlag:W}=d;if(B>0){if(B&128){ot(w,F,m,b,E,R,M,x,T);return}else if(B&256){Ie(w,F,m,b,E,R,M,x,T);return}}W&8?(j&16&&I(w,E,R),F!==w&&f(m,F)):j&16?W&16?ot(w,F,m,b,E,R,M,x,T):I(w,E,R,!0):(j&8&&f(m,""),W&16&&S(F,m,b,E,R,M,x,T))},Ie=(u,d,m,b,E,R,M,x,T)=>{u=u||Ct,d=d||Ct;const w=u.length,j=d.length,F=Math.min(w,j);let B;for(B=0;Bj?I(u,E,R,!0,!1,F):S(d,m,b,E,R,M,x,T,F)},ot=(u,d,m,b,E,R,M,x,T)=>{let w=0;const j=d.length;let F=u.length-1,B=j-1;for(;w<=F&&w<=B;){const W=u[w],X=d[w]=T?et(d[w]):Le(d[w]);if(dt(W,X))A(W,X,m,null,E,R,M,x,T);else break;w++}for(;w<=F&&w<=B;){const W=u[F],X=d[B]=T?et(d[B]):Le(d[B]);if(dt(W,X))A(W,X,m,null,E,R,M,x,T);else break;F--,B--}if(w>F){if(w<=B){const W=B+1,X=WB)for(;w<=F;)Ee(u[w],E,R,!0),w++;else{const W=w,X=w,le=new Map;for(w=X;w<=B;w++){const Re=d[w]=T?et(d[w]):Le(d[w]);Re.key!=null&&le.set(Re.key,w)}let ie,he=0;const Fe=B-X+1;let bt=!1,Ns=0;const kt=new Array(Fe);for(w=0;w=Fe){Ee(Re,E,R,!0);continue}let De;if(Re.key!=null)De=le.get(Re.key);else for(ie=X;ie<=B;ie++)if(kt[ie-X]===0&&dt(Re,d[ie])){De=ie;break}De===void 0?Ee(Re,E,R,!0):(kt[De-X]=w+1,De>=Ns?Ns=De:bt=!0,A(Re,d[De],m,null,E,R,M,x,T),he++)}const Fs=bt?Ll(kt):Ct;for(ie=Fs.length-1,w=Fe-1;w>=0;w--){const Re=X+w,De=d[Re],Ls=Re+1{const{el:R,type:M,transition:x,children:T,shapeFlag:w}=u;if(w&6){Ne(u.component.subTree,d,m,b);return}if(w&128){u.suspense.move(d,m,b);return}if(w&64){M.move(u,d,m,ee);return}if(M===we){s(R,d,m);for(let F=0;Fx.enter(R),E);else{const{leave:F,delayLeave:B,afterLeave:W}=x,X=()=>s(R,d,m),le=()=>{F(R,()=>{X(),W&&W()})};B?B(R,X,le):le()}else s(R,d,m)},Ee=(u,d,m,b=!1,E=!1)=>{const{type:R,props:M,ref:x,children:T,dynamicChildren:w,shapeFlag:j,patchFlag:F,dirs:B}=u;if(x!=null&&wn(x,null,m,u,!0),j&256){d.ctx.deactivate(u);return}const W=j&1&&B,X=!At(u);let le;if(X&&(le=M&&M.onVnodeBeforeUnmount)&&Oe(le,d,u),j&6)v(u.component,m,b);else{if(j&128){u.suspense.unmount(m,b);return}W&&Ue(u,null,d,"beforeUnmount"),j&64?u.type.remove(u,d,m,E,ee,b):w&&(R!==we||F>0&&F&64)?I(w,d,m,!1,!0):(R===we&&F&384||!E&&j&16)&&I(T,d,m),b&&yt(u)}(X&&(le=M&&M.onVnodeUnmounted)||W)&&Ce(()=>{le&&Oe(le,d,u),W&&Ue(u,null,d,"unmounted")},m)},yt=u=>{const{type:d,el:m,anchor:b,transition:E}=u;if(d===we){sn(m,b);return}if(d===Kt){$(u);return}const R=()=>{r(m),E&&!E.persisted&&E.afterLeave&&E.afterLeave()};if(u.shapeFlag&1&&E&&!E.persisted){const{leave:M,delayLeave:x}=E,T=()=>M(m,R);x?x(u.el,R,T):T()}else R()},sn=(u,d)=>{let m;for(;u!==d;)m=p(u),r(u),u=m;r(d)},v=(u,d,m)=>{const{bum:b,scope:E,update:R,subTree:M,um:x}=u;b&&jn(b),E.stop(),R&&(R.active=!1,Ee(M,u,d,m)),x&&Ce(x,d),Ce(()=>{u.isUnmounted=!0},d),d&&d.pendingBranch&&!d.isUnmounted&&u.asyncDep&&!u.asyncResolved&&u.suspenseId===d.pendingId&&(d.deps--,d.deps===0&&d.resolve())},I=(u,d,m,b=!1,E=!1,R=0)=>{for(let M=R;Mu.shapeFlag&6?O(u.component.subTree):u.shapeFlag&128?u.suspense.next():p(u.anchor||u.el),H=(u,d,m)=>{u==null?d._vnode&&Ee(d._vnode,null,null,!0):A(d._vnode||null,u,d,null,null,null,m),Ws(),vn(),d._vnode=u},ee={p:A,um:Ee,m:Ne,r:yt,mt:L,mc:S,pc:re,pbc:K,n:O,o:e};let fe,Y;return t&&([fe,Y]=t(ee)),{render:H,hydrate:fe,createApp:Ml(H,fe)}}function lt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function Pi(e,t,n=!1){const s=e.children,r=t.children;if(D(s)&&D(r))for(let i=0;i>1,e[n[l]]0&&(t[s]=n[i-1]),n[i]=s)}}for(i=n.length,o=n[i-1];i-- >0;)n[i]=o,o=t[o];return n}const kl=e=>e.__isTeleport,we=Symbol(void 0),Ot=Symbol(void 0),Ae=Symbol(void 0),Kt=Symbol(void 0),Wt=[];let $e=null;function Ai(e=!1){Wt.push($e=e?null:[])}function $l(){Wt.pop(),$e=Wt[Wt.length-1]||null}let Xt=1;function er(e){Xt+=e}function Ti(e){return e.dynamicChildren=Xt>0?$e||Ct:null,$l(),Xt>0&&$e&&$e.push(e),e}function Hu(e,t,n,s,r,i){return Ti(Mi(e,t,n,s,r,i,!0))}function Oi(e,t,n,s,r){return Ti(de(e,t,n,s,r,!0))}function xn(e){return e?e.__v_isVNode===!0:!1}function dt(e,t){return e.type===t.type&&e.key===t.key}const Fn="__vInternal",Si=({key:e})=>e!=null?e:null,mn=({ref:e,ref_key:t,ref_for:n})=>e!=null?pe(e)||me(e)||V(e)?{i:_e,r:e,k:t,f:!!n}:e:null;function Mi(e,t=null,n=null,s=0,r=null,i=e===we?0:1,o=!1,l=!1){const c={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&Si(t),ref:t&&mn(t),scopeId:li,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:i,patchFlag:s,dynamicProps:r,dynamicChildren:null,appContext:null,ctx:_e};return l?(Os(c,n),i&128&&e.normalize(c)):n&&(c.shapeFlag|=pe(n)?8:16),Xt>0&&!o&&$e&&(c.patchFlag>0||i&6)&&c.patchFlag!==32&&$e.push(c),c}const de=Hl;function Hl(e,t=null,n=null,s=0,r=null,i=!1){if((!e||e===_l)&&(e=Ae),xn(e)){const l=it(e,t,!0);return n&&Os(l,n),Xt>0&&!i&&$e&&(l.shapeFlag&6?$e[$e.indexOf(e)]=l:$e.push(l)),l.patchFlag|=-2,l}if(Jl(e)&&(e=e.__vccOpts),t){t=jl(t);let{class:l,style:c}=t;l&&!pe(l)&&(t.class=hs(l)),ue(c)&&(Yr(c)&&!D(c)&&(c=ge({},c)),t.style=ds(c))}const o=pe(e)?1:sl(e)?128:kl(e)?64:ue(e)?4:V(e)?2:0;return Mi(e,t,n,s,r,o,i,!0)}function jl(e){return e?Yr(e)||Fn in e?ge({},e):e:null}function it(e,t,n=!1){const{props:s,ref:r,patchFlag:i,children:o}=e,l=t?Bl(s||{},t):s;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:l,key:l&&Si(l),ref:t&&t.ref?n&&r?D(r)?r.concat(mn(t)):[r,mn(t)]:mn(t):r,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:o,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==we?i===-1?16:i|16:i,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&it(e.ssContent),ssFallback:e.ssFallback&&it(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Ii(e=" ",t=0){return de(Ot,null,e,t)}function ju(e,t){const n=de(Kt,null,e);return n.staticCount=t,n}function Bu(e="",t=!1){return t?(Ai(),Oi(Ae,null,e)):de(Ae,null,e)}function Le(e){return e==null||typeof e=="boolean"?de(Ae):D(e)?de(we,null,e.slice()):typeof e=="object"?et(e):de(Ot,null,String(e))}function et(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:it(e)}function Os(e,t){let n=0;const{shapeFlag:s}=e;if(t==null)t=null;else if(D(t))n=16;else if(typeof t=="object")if(s&65){const r=t.default;r&&(r._c&&(r._d=!1),Os(e,r()),r._c&&(r._d=!0));return}else{n=32;const r=t._;!r&&!(Fn in t)?t._ctx=_e:r===3&&_e&&(_e.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else V(t)?(t={default:t,_ctx:_e},n=32):(t=String(t),s&64?(n=16,t=[Ii(t)]):n=8);e.children=t,e.shapeFlag|=n}function Bl(...e){const t={};for(let n=0;nae||_e,St=e=>{ae=e,e.scope.on()},mt=()=>{ae&&ae.scope.off(),ae=null};function Ni(e){return e.vnode.shapeFlag&4}let Mt=!1;function ql(e,t=!1){Mt=t;const{props:n,children:s}=e.vnode,r=Ni(e);Rl(e,n,r,t),Tl(e,s);const i=r?Vl(e,t):void 0;return Mt=!1,i}function Vl(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Jr(new Proxy(e.ctx,bl));const{setup:s}=n;if(s){const r=e.setupContext=s.length>1?Ql(e):null;St(e),Ft();const i=st(s,e,0,[e.props,r]);if(Lt(),mt(),Fr(i)){if(i.then(mt,mt),t)return i.then(o=>{tr(e,o,t)}).catch(o=>{tn(o,e,0)});e.asyncDep=i}else tr(e,i,t)}else Fi(e,t)}function tr(e,t,n){V(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ue(t)&&(e.setupState=ei(t)),Fi(e,n)}let nr;function Fi(e,t,n){const s=e.type;if(!e.render){if(!t&&nr&&!s.render){const r=s.template||As(e).template;if(r){const{isCustomElement:i,compilerOptions:o}=e.appContext.config,{delimiters:l,compilerOptions:c}=s,a=ge(ge({isCustomElement:i,delimiters:l},o),c);s.render=nr(r,a)}}e.render=s.render||He}St(e),Ft(),vl(e),Lt(),mt()}function zl(e){return new Proxy(e.attrs,{get(t,n){return xe(e,"get","$attrs"),t[n]}})}function Ql(e){const t=s=>{e.exposed=s||{}};let n;return{get attrs(){return n||(n=zl(e))},slots:e.slots,emit:e.emit,expose:t}}function Ln(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(ei(Jr(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Ut)return Ut[n](e)},has(t,n){return n in t||n in Ut}}))}function Yl(e,t=!0){return V(e)?e.displayName||e.name:e.name||t&&e.__name}function Jl(e){return V(e)&&"__vccOpts"in e}const Se=(e,t)=>qo(e,t,Mt);function Ss(e,t,n){const s=arguments.length;return s===2?ue(t)&&!D(t)?xn(t)?de(e,null,[t]):de(e,t):de(e,null,t):(s>3?n=Array.prototype.slice.call(arguments,2):s===3&&xn(n)&&(n=[n]),de(e,t,n))}const Xl=Symbol(""),Zl=()=>je(Xl),Gl="3.2.47",ec="http://www.w3.org/2000/svg",ht=typeof document<"u"?document:null,sr=ht&&ht.createElement("template"),tc={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,s)=>{const r=t?ht.createElementNS(ec,e):ht.createElement(e,n?{is:n}:void 0);return e==="select"&&s&&s.multiple!=null&&r.setAttribute("multiple",s.multiple),r},createText:e=>ht.createTextNode(e),createComment:e=>ht.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ht.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,s,r,i){const o=n?n.previousSibling:t.lastChild;if(r&&(r===i||r.nextSibling))for(;t.insertBefore(r.cloneNode(!0),n),!(r===i||!(r=r.nextSibling)););else{sr.innerHTML=s?`${e}`:e;const l=sr.content;if(s){const c=l.firstChild;for(;c.firstChild;)l.appendChild(c.firstChild);l.removeChild(c)}t.insertBefore(l,n)}return[o?o.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function nc(e,t,n){const s=e._vtc;s&&(t=(t?[t,...s]:[...s]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function sc(e,t,n){const s=e.style,r=pe(n);if(n&&!r){if(t&&!pe(t))for(const i in t)n[i]==null&&ls(s,i,"");for(const i in n)ls(s,i,n[i])}else{const i=s.display;r?t!==n&&(s.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(s.display=i)}}const rr=/\s*!important$/;function ls(e,t,n){if(D(n))n.forEach(s=>ls(e,t,s));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const s=rc(e,t);rr.test(n)?e.setProperty(_t(s),n.replace(rr,""),"important"):e[s]=n}}const ir=["Webkit","Moz","ms"],Wn={};function rc(e,t){const n=Wn[t];if(n)return n;let s=We(t);if(s!=="filter"&&s in e)return Wn[t]=s;s=Tn(s);for(let r=0;rqn||(ac.then(()=>qn=0),qn=Date.now());function hc(e,t){const n=s=>{if(!s._vts)s._vts=Date.now();else if(s._vts<=n.attached)return;Me(pc(s,n.value),t,5,[s])};return n.value=e,n.attached=dc(),n}function pc(e,t){if(D(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(s=>r=>!r._stopped&&s&&s(r))}else return t}const cr=/^on[a-z]/,gc=(e,t,n,s,r=!1,i,o,l,c)=>{t==="class"?nc(e,s,r):t==="style"?sc(e,n,s):Gt(t)?ps(t)||uc(e,t,n,s,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):mc(e,t,s,r))?oc(e,t,s,i,o,l,c):(t==="true-value"?e._trueValue=s:t==="false-value"&&(e._falseValue=s),ic(e,t,s,r))};function mc(e,t,n,s){return s?!!(t==="innerHTML"||t==="textContent"||t in e&&cr.test(t)&&V(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||cr.test(t)&&pe(n)?!1:t in e}const Xe="transition",$t="animation",Li=(e,{slots:t})=>Ss(fi,_c(e),t);Li.displayName="Transition";const ki={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};Li.props=ge({},fi.props,ki);const ct=(e,t=[])=>{D(e)?e.forEach(n=>n(...t)):e&&e(...t)},ur=e=>e?D(e)?e.some(t=>t.length>1):e.length>1:!1;function _c(e){const t={};for(const N in e)N in ki||(t[N]=e[N]);if(e.css===!1)return t;const{name:n="v",type:s,duration:r,enterFromClass:i=`${n}-enter-from`,enterActiveClass:o=`${n}-enter-active`,enterToClass:l=`${n}-enter-to`,appearFromClass:c=i,appearActiveClass:a=o,appearToClass:f=l,leaveFromClass:h=`${n}-leave-from`,leaveActiveClass:p=`${n}-leave-active`,leaveToClass:_=`${n}-leave-to`}=e,C=yc(r),A=C&&C[0],k=C&&C[1],{onBeforeEnter:g,onEnter:y,onEnterCancelled:P,onLeave:$,onLeaveCancelled:U,onBeforeAppear:J=g,onAppear:z=y,onAppearCancelled:S=P}=t,q=(N,Q,L)=>{ut(N,Q?f:l),ut(N,Q?a:o),L&&L()},K=(N,Q)=>{N._isLeaving=!1,ut(N,h),ut(N,_),ut(N,p),Q&&Q()},Z=N=>(Q,L)=>{const ye=N?z:y,G=()=>q(Q,N,L);ct(ye,[Q,G]),fr(()=>{ut(Q,N?c:i),Ze(Q,N?f:l),ur(ye)||ar(Q,s,A,G)})};return ge(t,{onBeforeEnter(N){ct(g,[N]),Ze(N,i),Ze(N,o)},onBeforeAppear(N){ct(J,[N]),Ze(N,c),Ze(N,a)},onEnter:Z(!1),onAppear:Z(!0),onLeave(N,Q){N._isLeaving=!0;const L=()=>K(N,Q);Ze(N,h),Ec(),Ze(N,p),fr(()=>{!N._isLeaving||(ut(N,h),Ze(N,_),ur($)||ar(N,s,k,L))}),ct($,[N,L])},onEnterCancelled(N){q(N,!1),ct(P,[N])},onAppearCancelled(N){q(N,!0),ct(S,[N])},onLeaveCancelled(N){K(N),ct(U,[N])}})}function yc(e){if(e==null)return null;if(ue(e))return[Vn(e.enter),Vn(e.leave)];{const t=Vn(e);return[t,t]}}function Vn(e){return oo(e)}function Ze(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e._vtc||(e._vtc=new Set)).add(t)}function ut(e,t){t.split(/\s+/).forEach(s=>s&&e.classList.remove(s));const{_vtc:n}=e;n&&(n.delete(t),n.size||(e._vtc=void 0))}function fr(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let bc=0;function ar(e,t,n,s){const r=e._endId=++bc,i=()=>{r===e._endId&&s()};if(n)return setTimeout(i,n);const{type:o,timeout:l,propCount:c}=vc(e,t);if(!o)return s();const a=o+"end";let f=0;const h=()=>{e.removeEventListener(a,p),i()},p=_=>{_.target===e&&++f>=c&&h()};setTimeout(()=>{f(n[C]||"").split(", "),r=s(`${Xe}Delay`),i=s(`${Xe}Duration`),o=dr(r,i),l=s(`${$t}Delay`),c=s(`${$t}Duration`),a=dr(l,c);let f=null,h=0,p=0;t===Xe?o>0&&(f=Xe,h=o,p=i.length):t===$t?a>0&&(f=$t,h=a,p=c.length):(h=Math.max(o,a),f=h>0?o>a?Xe:$t:null,p=f?f===Xe?i.length:c.length:0);const _=f===Xe&&/\b(transform|all)(,|$)/.test(s(`${Xe}Property`).toString());return{type:f,timeout:h,propCount:p,hasTransform:_}}function dr(e,t){for(;e.lengthhr(n)+hr(e[s])))}function hr(e){return Number(e.slice(0,-1).replace(",","."))*1e3}function Ec(){return document.body.offsetHeight}const Cc={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Du=(e,t)=>n=>{if(!("key"in n))return;const s=_t(n.key);if(t.some(r=>r===s||Cc[r]===s))return e(n)},Uu={beforeMount(e,{value:t},{transition:n}){e._vod=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):Ht(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:s}){!t!=!n&&(s?t?(s.beforeEnter(e),Ht(e,!0),s.enter(e)):s.leave(e,()=>{Ht(e,!1)}):Ht(e,t))},beforeUnmount(e,{value:t}){Ht(e,t)}};function Ht(e,t){e.style.display=t?e._vod:"none"}const wc=ge({patchProp:gc},tc);let zn,pr=!1;function xc(){return zn=pr?zn:Nl(wc),pr=!0,zn}const Ku=(...e)=>{const t=xc().createApp(...e),{mount:n}=t;return t.mount=s=>{const r=Rc(s);if(r)return n(r,!0,r instanceof SVGElement)},t};function Rc(e){return pe(e)?document.querySelector(e):e}var Pc=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),Wu=e=>{const t=new Set,n=[];return e.forEach(s=>{const r=Pc(s);t.has(r)||(t.add(r),n.push(s))}),n},qu=e=>/^(https?:)?\/\//.test(e),Vu=e=>/^mailto:/.test(e),zu=e=>/^tel:/.test(e),Qu=e=>Object.prototype.toString.call(e)==="[object Object]",Yu=e=>e.replace(/\/$/,""),Ju=e=>e.replace(/^\//,""),Xu=(e,t)=>{const n=Object.keys(e).sort((s,r)=>{const i=r.split("/").length-s.split("/").length;return i!==0?i:r.length-s.length});for(const s of n)if(t.startsWith(s))return s;return"/"},Zu=(e,t="/")=>e.replace(/^(https?:)?\/\/[^/]*/,"").replace(new RegExp(`^${t}`),"/");/*! + * vue-router v4.1.6 + * (c) 2022 Eduardo San Martin Morote + * @license MIT + */const Et=typeof window<"u";function Ac(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const se=Object.assign;function Qn(e,t){const n={};for(const s in t){const r=t[s];n[s]=Be(r)?r.map(e):e(r)}return n}const qt=()=>{},Be=Array.isArray,Tc=/\/$/,Oc=e=>e.replace(Tc,"");function Yn(e,t,n="/"){let s,r={},i="",o="";const l=t.indexOf("#");let c=t.indexOf("?");return l=0&&(c=-1),c>-1&&(s=t.slice(0,c),i=t.slice(c+1,l>-1?l:t.length),r=e(i)),l>-1&&(s=s||t.slice(0,l),o=t.slice(l,t.length)),s=Nc(s!=null?s:t,n),{fullPath:s+(i&&"?")+i+o,path:s,query:r,hash:o}}function Sc(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function gr(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Mc(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&It(t.matched[s],n.matched[r])&&$i(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function It(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function $i(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!Ic(e[n],t[n]))return!1;return!0}function Ic(e,t){return Be(e)?mr(e,t):Be(t)?mr(t,e):e===t}function mr(e,t){return Be(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function Nc(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/");let r=n.length-1,i,o;for(i=0;i1&&r--;else break;return n.slice(0,r).join("/")+"/"+s.slice(i-(i===s.length?1:0)).join("/")}var Zt;(function(e){e.pop="pop",e.push="push"})(Zt||(Zt={}));var Vt;(function(e){e.back="back",e.forward="forward",e.unknown=""})(Vt||(Vt={}));function Fc(e){if(!e)if(Et){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Oc(e)}const Lc=/^[^#]+#/;function kc(e,t){return e.replace(Lc,"#")+t}function $c(e,t){const n=document.documentElement.getBoundingClientRect(),s=e.getBoundingClientRect();return{behavior:t.behavior,left:s.left-n.left-(t.left||0),top:s.top-n.top-(t.top||0)}}const kn=()=>({left:window.pageXOffset,top:window.pageYOffset});function Hc(e){let t;if("el"in e){const n=e.el,s=typeof n=="string"&&n.startsWith("#"),r=typeof n=="string"?s?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!r)return;t=$c(r,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.pageXOffset,t.top!=null?t.top:window.pageYOffset)}function _r(e,t){return(history.state?history.state.position-t:-1)+e}const cs=new Map;function jc(e,t){cs.set(e,t)}function Bc(e){const t=cs.get(e);return cs.delete(e),t}let Dc=()=>location.protocol+"//"+location.host;function Hi(e,t){const{pathname:n,search:s,hash:r}=t,i=e.indexOf("#");if(i>-1){let l=r.includes(e.slice(i))?e.slice(i).length:1,c=r.slice(l);return c[0]!=="/"&&(c="/"+c),gr(c,"")}return gr(n,e)+s+r}function Uc(e,t,n,s){let r=[],i=[],o=null;const l=({state:p})=>{const _=Hi(e,location),C=n.value,A=t.value;let k=0;if(p){if(n.value=_,t.value=p,o&&o===C){o=null;return}k=A?p.position-A.position:0}else s(_);r.forEach(g=>{g(n.value,C,{delta:k,type:Zt.pop,direction:k?k>0?Vt.forward:Vt.back:Vt.unknown})})};function c(){o=n.value}function a(p){r.push(p);const _=()=>{const C=r.indexOf(p);C>-1&&r.splice(C,1)};return i.push(_),_}function f(){const{history:p}=window;!p.state||p.replaceState(se({},p.state,{scroll:kn()}),"")}function h(){for(const p of i)p();i=[],window.removeEventListener("popstate",l),window.removeEventListener("beforeunload",f)}return window.addEventListener("popstate",l),window.addEventListener("beforeunload",f),{pauseListeners:c,listen:a,destroy:h}}function yr(e,t,n,s=!1,r=!1){return{back:e,current:t,forward:n,replaced:s,position:window.history.length,scroll:r?kn():null}}function Kc(e){const{history:t,location:n}=window,s={value:Hi(e,n)},r={value:t.state};r.value||i(s.value,{back:null,current:s.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(c,a,f){const h=e.indexOf("#"),p=h>-1?(n.host&&document.querySelector("base")?e:e.slice(h))+c:Dc()+e+c;try{t[f?"replaceState":"pushState"](a,"",p),r.value=a}catch(_){console.error(_),n[f?"replace":"assign"](p)}}function o(c,a){const f=se({},t.state,yr(r.value.back,c,r.value.forward,!0),a,{position:r.value.position});i(c,f,!0),s.value=c}function l(c,a){const f=se({},r.value,t.state,{forward:c,scroll:kn()});i(f.current,f,!0);const h=se({},yr(s.value,c,null),{position:f.position+1},a);i(c,h,!1),s.value=c}return{location:s,state:r,push:l,replace:o}}function Gu(e){e=Fc(e);const t=Kc(e),n=Uc(e,t.state,t.location,t.replace);function s(i,o=!0){o||n.pauseListeners(),history.go(i)}const r=se({location:"",base:e,go:s,createHref:kc.bind(null,e)},t,n);return Object.defineProperty(r,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(r,"state",{enumerable:!0,get:()=>t.state.value}),r}function Wc(e){return typeof e=="string"||e&&typeof e=="object"}function ji(e){return typeof e=="string"||typeof e=="symbol"}const Ge={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Bi=Symbol("");var br;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(br||(br={}));function Nt(e,t){return se(new Error,{type:e,[Bi]:!0},t)}function qe(e,t){return e instanceof Error&&Bi in e&&(t==null||!!(e.type&t))}const vr="[^/]+?",qc={sensitive:!1,strict:!1,start:!0,end:!0},Vc=/[.+*?^${}()[\]/\\]/g;function zc(e,t){const n=se({},qc,t),s=[];let r=n.start?"^":"";const i=[];for(const a of e){const f=a.length?[]:[90];n.strict&&!a.length&&(r+="/");for(let h=0;ht.length?t.length===1&&t[0]===40+40?1:-1:0}function Yc(e,t){let n=0;const s=e.score,r=t.score;for(;n0&&t[t.length-1]<0}const Jc={type:0,value:""},Xc=/[a-zA-Z0-9_]/;function Zc(e){if(!e)return[[]];if(e==="/")return[[Jc]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(_){throw new Error(`ERR (${n})/"${a}": ${_}`)}let n=0,s=n;const r=[];let i;function o(){i&&r.push(i),i=[]}let l=0,c,a="",f="";function h(){!a||(n===0?i.push({type:0,value:a}):n===1||n===2||n===3?(i.length>1&&(c==="*"||c==="+")&&t(`A repeatable param (${a}) must be alone in its segment. eg: '/:ids+.`),i.push({type:1,value:a,regexp:f,repeatable:c==="*"||c==="+",optional:c==="*"||c==="?"})):t("Invalid state to consume buffer"),a="")}function p(){a+=c}for(;l{o(y)}:qt}function o(f){if(ji(f)){const h=s.get(f);h&&(s.delete(f),n.splice(n.indexOf(h),1),h.children.forEach(o),h.alias.forEach(o))}else{const h=n.indexOf(f);h>-1&&(n.splice(h,1),f.record.name&&s.delete(f.record.name),f.children.forEach(o),f.alias.forEach(o))}}function l(){return n}function c(f){let h=0;for(;h=0&&(f.record.path!==n[h].record.path||!Di(f,n[h]));)h++;n.splice(h,0,f),f.record.name&&!wr(f)&&s.set(f.record.name,f)}function a(f,h){let p,_={},C,A;if("name"in f&&f.name){if(p=s.get(f.name),!p)throw Nt(1,{location:f});A=p.record.name,_=se(Cr(h.params,p.keys.filter(y=>!y.optional).map(y=>y.name)),f.params&&Cr(f.params,p.keys.map(y=>y.name))),C=p.stringify(_)}else if("path"in f)C=f.path,p=n.find(y=>y.re.test(C)),p&&(_=p.parse(C),A=p.record.name);else{if(p=h.name?s.get(h.name):n.find(y=>y.re.test(h.path)),!p)throw Nt(1,{location:f,currentLocation:h});A=p.record.name,_=se({},h.params,f.params),C=p.stringify(_)}const k=[];let g=p;for(;g;)k.unshift(g.record),g=g.parent;return{name:A,path:C,params:_,matched:k,meta:su(k)}}return e.forEach(f=>i(f)),{addRoute:i,resolve:a,removeRoute:o,getRoutes:l,getRecordMatcher:r}}function Cr(e,t){const n={};for(const s of t)s in e&&(n[s]=e[s]);return n}function tu(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:nu(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function nu(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const s in e.components)t[s]=typeof n=="boolean"?n:n[s];return t}function wr(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function su(e){return e.reduce((t,n)=>se(t,n.meta),{})}function xr(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}function Di(e,t){return t.children.some(n=>n===e||Di(e,n))}const Ui=/#/g,ru=/&/g,iu=/\//g,ou=/=/g,lu=/\?/g,Ki=/\+/g,cu=/%5B/g,uu=/%5D/g,Wi=/%5E/g,fu=/%60/g,qi=/%7B/g,au=/%7C/g,Vi=/%7D/g,du=/%20/g;function Ms(e){return encodeURI(""+e).replace(au,"|").replace(cu,"[").replace(uu,"]")}function hu(e){return Ms(e).replace(qi,"{").replace(Vi,"}").replace(Wi,"^")}function us(e){return Ms(e).replace(Ki,"%2B").replace(du,"+").replace(Ui,"%23").replace(ru,"%26").replace(fu,"`").replace(qi,"{").replace(Vi,"}").replace(Wi,"^")}function pu(e){return us(e).replace(ou,"%3D")}function gu(e){return Ms(e).replace(Ui,"%23").replace(lu,"%3F")}function mu(e){return e==null?"":gu(e).replace(iu,"%2F")}function Rn(e){try{return decodeURIComponent(""+e)}catch{}return""+e}function _u(e){const t={};if(e===""||e==="?")return t;const s=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;ri&&us(i)):[s&&us(s)]).forEach(i=>{i!==void 0&&(t+=(t.length?"&":"")+n,i!=null&&(t+="="+i))})}return t}function yu(e){const t={};for(const n in e){const s=e[n];s!==void 0&&(t[n]=Be(s)?s.map(r=>r==null?null:""+r):s==null?s:""+s)}return t}const bu=Symbol(""),Pr=Symbol(""),$n=Symbol(""),Is=Symbol(""),fs=Symbol("");function jt(){let e=[];function t(s){return e.push(s),()=>{const r=e.indexOf(s);r>-1&&e.splice(r,1)}}function n(){e=[]}return{add:t,list:()=>e,reset:n}}function tt(e,t,n,s,r){const i=s&&(s.enterCallbacks[r]=s.enterCallbacks[r]||[]);return()=>new Promise((o,l)=>{const c=h=>{h===!1?l(Nt(4,{from:n,to:t})):h instanceof Error?l(h):Wc(h)?l(Nt(2,{from:t,to:h})):(i&&s.enterCallbacks[r]===i&&typeof h=="function"&&i.push(h),o())},a=e.call(s&&s.instances[r],t,n,c);let f=Promise.resolve(a);e.length<3&&(f=f.then(c)),f.catch(h=>l(h))})}function Jn(e,t,n,s){const r=[];for(const i of e)for(const o in i.components){let l=i.components[o];if(!(t!=="beforeRouteEnter"&&!i.instances[o]))if(vu(l)){const a=(l.__vccOpts||l)[t];a&&r.push(tt(a,n,s,i,o))}else{let c=l();r.push(()=>c.then(a=>{if(!a)return Promise.reject(new Error(`Couldn't resolve component "${o}" at "${i.path}"`));const f=Ac(a)?a.default:a;i.components[o]=f;const p=(f.__vccOpts||f)[t];return p&&tt(p,n,s,i,o)()}))}}return r}function vu(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function Ar(e){const t=je($n),n=je(Is),s=Se(()=>t.resolve(Rt(e.to))),r=Se(()=>{const{matched:c}=s.value,{length:a}=c,f=c[a-1],h=n.matched;if(!f||!h.length)return-1;const p=h.findIndex(It.bind(null,f));if(p>-1)return p;const _=Tr(c[a-2]);return a>1&&Tr(f)===_&&h[h.length-1].path!==_?h.findIndex(It.bind(null,c[a-2])):p}),i=Se(()=>r.value>-1&&xu(n.params,s.value.params)),o=Se(()=>r.value>-1&&r.value===n.matched.length-1&&$i(n.params,s.value.params));function l(c={}){return wu(c)?t[Rt(e.replace)?"replace":"push"](Rt(e.to)).catch(qt):Promise.resolve()}return{route:s,href:Se(()=>s.value.href),isActive:i,isExactActive:o,navigate:l}}const Eu=Ps({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:Ar,setup(e,{slots:t}){const n=en(Ar(e)),{options:s}=je($n),r=Se(()=>({[Or(e.activeClass,s.linkActiveClass,"router-link-active")]:n.isActive,[Or(e.exactActiveClass,s.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const i=t.default&&t.default(n);return e.custom?i:Ss("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:r.value},i)}}}),Cu=Eu;function wu(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function xu(e,t){for(const n in t){const s=t[n],r=e[n];if(typeof s=="string"){if(s!==r)return!1}else if(!Be(r)||r.length!==s.length||s.some((i,o)=>i!==r[o]))return!1}return!0}function Tr(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Or=(e,t,n)=>e!=null?e:t!=null?t:n,Ru=Ps({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const s=je(fs),r=Se(()=>e.route||s.value),i=je(Pr,0),o=Se(()=>{let a=Rt(i);const{matched:f}=r.value;let h;for(;(h=f[a])&&!h.components;)a++;return a}),l=Se(()=>r.value.matched[o.value]);pn(Pr,Se(()=>o.value+1)),pn(bu,l),pn(fs,r);const c=hn();return gn(()=>[c.value,l.value,e.name],([a,f,h],[p,_,C])=>{f&&(f.instances[h]=a,_&&_!==f&&a&&a===p&&(f.leaveGuards.size||(f.leaveGuards=_.leaveGuards),f.updateGuards.size||(f.updateGuards=_.updateGuards))),a&&f&&(!_||!It(f,_)||!p)&&(f.enterCallbacks[h]||[]).forEach(A=>A(a))},{flush:"post"}),()=>{const a=r.value,f=e.name,h=l.value,p=h&&h.components[f];if(!p)return Sr(n.default,{Component:p,route:a});const _=h.props[f],C=_?_===!0?a.params:typeof _=="function"?_(a):_:null,k=Ss(p,se({},C,t,{onVnodeUnmounted:g=>{g.component.isUnmounted&&(h.instances[f]=null)},ref:c}));return Sr(n.default,{Component:k,route:a})||k}}});function Sr(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Pu=Ru;function ef(e){const t=eu(e.routes,e),n=e.parseQuery||_u,s=e.stringifyQuery||Rr,r=e.history,i=jt(),o=jt(),l=jt(),c=jo(Ge);let a=Ge;Et&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const f=Qn.bind(null,v=>""+v),h=Qn.bind(null,mu),p=Qn.bind(null,Rn);function _(v,I){let O,H;return ji(v)?(O=t.getRecordMatcher(v),H=I):H=v,t.addRoute(H,O)}function C(v){const I=t.getRecordMatcher(v);I&&t.removeRoute(I)}function A(){return t.getRoutes().map(v=>v.record)}function k(v){return!!t.getRecordMatcher(v)}function g(v,I){if(I=se({},I||c.value),typeof v=="string"){const u=Yn(n,v,I.path),d=t.resolve({path:u.path},I),m=r.createHref(u.fullPath);return se(u,d,{params:p(d.params),hash:Rn(u.hash),redirectedFrom:void 0,href:m})}let O;if("path"in v)O=se({},v,{path:Yn(n,v.path,I.path).path});else{const u=se({},v.params);for(const d in u)u[d]==null&&delete u[d];O=se({},v,{params:h(v.params)}),I.params=h(I.params)}const H=t.resolve(O,I),ee=v.hash||"";H.params=f(p(H.params));const fe=Sc(s,se({},v,{hash:hu(ee),path:H.path})),Y=r.createHref(fe);return se({fullPath:fe,hash:ee,query:s===Rr?yu(v.query):v.query||{}},H,{redirectedFrom:void 0,href:Y})}function y(v){return typeof v=="string"?Yn(n,v,c.value.path):se({},v)}function P(v,I){if(a!==v)return Nt(8,{from:I,to:v})}function $(v){return z(v)}function U(v){return $(se(y(v),{replace:!0}))}function J(v){const I=v.matched[v.matched.length-1];if(I&&I.redirect){const{redirect:O}=I;let H=typeof O=="function"?O(v):O;return typeof H=="string"&&(H=H.includes("?")||H.includes("#")?H=y(H):{path:H},H.params={}),se({query:v.query,hash:v.hash,params:"path"in H?{}:v.params},H)}}function z(v,I){const O=a=g(v),H=c.value,ee=v.state,fe=v.force,Y=v.replace===!0,u=J(O);if(u)return z(se(y(u),{state:typeof u=="object"?se({},ee,u.state):ee,force:fe,replace:Y}),I||O);const d=O;d.redirectedFrom=I;let m;return!fe&&Mc(s,H,O)&&(m=Nt(16,{to:d,from:H}),ot(H,H,!0,!1)),(m?Promise.resolve(m):q(d,H)).catch(b=>qe(b)?qe(b,2)?b:Ie(b):oe(b,d,H)).then(b=>{if(b){if(qe(b,2))return z(se({replace:Y},y(b.to),{state:typeof b.to=="object"?se({},ee,b.to.state):ee,force:fe}),I||d)}else b=Z(d,H,!0,Y,ee);return K(d,H,b),b})}function S(v,I){const O=P(v,I);return O?Promise.reject(O):Promise.resolve()}function q(v,I){let O;const[H,ee,fe]=Au(v,I);O=Jn(H.reverse(),"beforeRouteLeave",v,I);for(const u of H)u.leaveGuards.forEach(d=>{O.push(tt(d,v,I))});const Y=S.bind(null,v,I);return O.push(Y),vt(O).then(()=>{O=[];for(const u of i.list())O.push(tt(u,v,I));return O.push(Y),vt(O)}).then(()=>{O=Jn(ee,"beforeRouteUpdate",v,I);for(const u of ee)u.updateGuards.forEach(d=>{O.push(tt(d,v,I))});return O.push(Y),vt(O)}).then(()=>{O=[];for(const u of v.matched)if(u.beforeEnter&&!I.matched.includes(u))if(Be(u.beforeEnter))for(const d of u.beforeEnter)O.push(tt(d,v,I));else O.push(tt(u.beforeEnter,v,I));return O.push(Y),vt(O)}).then(()=>(v.matched.forEach(u=>u.enterCallbacks={}),O=Jn(fe,"beforeRouteEnter",v,I),O.push(Y),vt(O))).then(()=>{O=[];for(const u of o.list())O.push(tt(u,v,I));return O.push(Y),vt(O)}).catch(u=>qe(u,8)?u:Promise.reject(u))}function K(v,I,O){for(const H of l.list())H(v,I,O)}function Z(v,I,O,H,ee){const fe=P(v,I);if(fe)return fe;const Y=I===Ge,u=Et?history.state:{};O&&(H||Y?r.replace(v.fullPath,se({scroll:Y&&u&&u.scroll},ee)):r.push(v.fullPath,ee)),c.value=v,ot(v,I,O,Y),Ie()}let N;function Q(){N||(N=r.listen((v,I,O)=>{if(!sn.listening)return;const H=g(v),ee=J(H);if(ee){z(se(ee,{replace:!0}),H).catch(qt);return}a=H;const fe=c.value;Et&&jc(_r(fe.fullPath,O.delta),kn()),q(H,fe).catch(Y=>qe(Y,12)?Y:qe(Y,2)?(z(Y.to,H).then(u=>{qe(u,20)&&!O.delta&&O.type===Zt.pop&&r.go(-1,!1)}).catch(qt),Promise.reject()):(O.delta&&r.go(-O.delta,!1),oe(Y,H,fe))).then(Y=>{Y=Y||Z(H,fe,!1),Y&&(O.delta&&!qe(Y,8)?r.go(-O.delta,!1):O.type===Zt.pop&&qe(Y,20)&&r.go(-1,!1)),K(H,fe,Y)}).catch(qt)}))}let L=jt(),ye=jt(),G;function oe(v,I,O){Ie(v);const H=ye.list();return H.length?H.forEach(ee=>ee(v,I,O)):console.error(v),Promise.reject(v)}function re(){return G&&c.value!==Ge?Promise.resolve():new Promise((v,I)=>{L.add([v,I])})}function Ie(v){return G||(G=!v,Q(),L.list().forEach(([I,O])=>v?O(v):I()),L.reset()),v}function ot(v,I,O,H){const{scrollBehavior:ee}=e;if(!Et||!ee)return Promise.resolve();const fe=!O&&Bc(_r(v.fullPath,0))||(H||!O)&&history.state&&history.state.scroll||null;return si().then(()=>ee(v,I,fe)).then(Y=>Y&&Hc(Y)).catch(Y=>oe(Y,v,I))}const Ne=v=>r.go(v);let Ee;const yt=new Set,sn={currentRoute:c,listening:!0,addRoute:_,removeRoute:C,hasRoute:k,getRoutes:A,resolve:g,options:e,push:$,replace:U,go:Ne,back:()=>Ne(-1),forward:()=>Ne(1),beforeEach:i.add,beforeResolve:o.add,afterEach:l.add,onError:ye.add,isReady:re,install(v){const I=this;v.component("RouterLink",Cu),v.component("RouterView",Pu),v.config.globalProperties.$router=I,Object.defineProperty(v.config.globalProperties,"$route",{enumerable:!0,get:()=>Rt(c)}),Et&&!Ee&&c.value===Ge&&(Ee=!0,$(r.location).catch(ee=>{}));const O={};for(const ee in Ge)O[ee]=Se(()=>c.value[ee]);v.provide($n,I),v.provide(Is,en(O)),v.provide(fs,c);const H=v.unmount;yt.add(v),v.unmount=function(){yt.delete(v),yt.size<1&&(a=Ge,N&&N(),N=null,c.value=Ge,Ee=!1,G=!1),H()}}};return sn}function vt(e){return e.reduce((t,n)=>t.then(()=>n()),Promise.resolve())}function Au(e,t){const n=[],s=[],r=[],i=Math.max(t.matched.length,e.matched.length);for(let o=0;oIt(a,l))?s.push(l):n.push(l));const c=e.matched[o];c&&(t.matched.find(a=>It(a,c))||r.push(c))}return[n,s,r]}function tf(){return je($n)}function nf(){return je(Is)}const sf=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n};export{Zo as $,$u as A,Ii as B,Tu as C,hs as D,ds as E,me as F,Rt as G,uo as H,Ou as I,Wl as J,si as K,jo as L,Iu as M,mi as N,V as O,Lu as P,de as Q,Pu as R,Ge as S,Li as T,we as U,ku as V,Bu as W,Mi as X,Mu as Y,Oi as Z,sf as _,Qr as a,Bl as a0,Vu as a1,zu as a2,Fu as a3,Uu as a4,ju as a5,gi as a6,Du as a7,Zu as a8,Su as a9,en as b,Ps as c,Nu as d,D as e,Wu as f,Xu as g,Se as h,pe as i,Ss as j,je as k,qu as l,Ju as m,Ku as n,pi as o,ef as p,Yu as q,hn as r,Gu as s,Qu as t,nf as u,pn as v,gn as w,tf as x,Ai as y,Hu as z}; diff --git a/assets/generation.html.c0787daf.js b/assets/generation.html.c0787daf.js new file mode 100644 index 000000000..118199397 --- /dev/null +++ b/assets/generation.html.c0787daf.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-7231735f","path":"/stacks/agnostic/generation.html","title":"Code Generation Overview","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Running Code Generation","slug":"running-code-generation","link":"#running-code-generation","children":[{"level":3,"title":"CLI Options","slug":"cli-options","link":"#cli-options","children":[]}]},{"level":2,"title":"Generated Code","slug":"generated-code","link":"#generated-code","children":[{"level":3,"title":"Backend C#","slug":"backend-c","link":"#backend-c","children":[]},{"level":3,"title":"Frontend - Vue","slug":"frontend-vue","link":"#frontend-vue","children":[]},{"level":3,"title":"Frontend - Knockout","slug":"frontend-knockout","link":"#frontend-knockout","children":[]}]}],"git":{"updatedTime":1678412142000},"filePathRelative":"stacks/agnostic/generation.md"}');export{e as data}; diff --git a/assets/generation.html.d81ac009.js b/assets/generation.html.d81ac009.js new file mode 100644 index 000000000..535dd2483 --- /dev/null +++ b/assets/generation.html.d81ac009.js @@ -0,0 +1,19 @@ +import{_ as c,y as i,z as p,X as n,Q as o,$ as s,B as e,a5 as r,P as l}from"./framework.fe9a73df.js";const d={},u=n("h1",{id:"code-generation-overview",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#code-generation-overview","aria-hidden":"true"},"#"),e(" Code Generation Overview")],-1),h=n("p",null,"Coalesce's principal purpose is a code generation framework for automating the creation of the boring-but-necessary parts of a web application. Below, you find an overview of the different components of Coalesce's code generation features.",-1),y={class:"table-of-contents"},D=r(`

Running Code Generation

Coalesce's code generation is ran via a dotnet CLI tool, dotnet coalesce. In order to invoke this tool, you must have the appropriate references to the package that provides it in your .csproj file:

<Project Sdk="Microsoft.NET.Sdk.Web">
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+
+    <!-- Necessary to use DotNetCliToolReference with modern framework versions -->
+    <DotnetCliToolTargetFramework>net7.0</DotnetCliToolTargetFramework>
+  </PropertyGroup>
+
+  ...
+
+  <ItemGroup>
+    <PackageReference Include="IntelliTect.Coalesce" Version="..." />
+  </ItemGroup>
+
+  <ItemGroup>
+    <DotNetCliToolReference Include="IntelliTect.Coalesce.Tools" Version="..." />
+  </ItemGroup>  
+</Project>
+

CLI Options

`,4),f=n("code",null,"coalesce.json",-1),v=r('

There are a couple of extra options which are only available as CLI parameters to dotnet coalesce. These options do not affect the behavior of the code generation - only the behavior of the CLI itself.

--debug - When this flag is specified when running dotnet coalesce, Coalesce will wait for a debugger to be attached to its process before starting code generation.

-v|--verbosity <level> - Set the verbosity of the output. Options are trace, debug, information, warning, error, critical, and none.

Generated Code

Coalesce will generate a full vertical stack of code for you:

Backend C#

API Controllers

',7),m=n("code",null,"/Api/Generated",-1),g=n("h4",{id:"c-dtos",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#c-dtos","aria-hidden":"true"},"#"),e(" C# DTOs")],-1),b=n("h3",{id:"frontend-vue",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#frontend-vue","aria-hidden":"true"},"#"),e(" Frontend - Vue")],-1),C=n("h3",{id:"frontend-knockout",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#frontend-knockout","aria-hidden":"true"},"#"),e(" Frontend - Knockout")],-1);function _(k,w){const t=l("router-link"),a=l("RouterLink");return i(),p("div",null,[u,h,n("nav",y,[n("ul",null,[n("li",null,[o(t,{to:"#running-code-generation"},{default:s(()=>[e("Running Code Generation")]),_:1}),n("ul",null,[n("li",null,[o(t,{to:"#cli-options"},{default:s(()=>[e("CLI Options")]),_:1})])])]),n("li",null,[o(t,{to:"#generated-code"},{default:s(()=>[e("Generated Code")]),_:1}),n("ul",null,[n("li",null,[o(t,{to:"#backend-c"},{default:s(()=>[e("Backend C#")]),_:1})]),n("li",null,[o(t,{to:"#frontend-vue"},{default:s(()=>[e("Frontend - Vue")]),_:1})]),n("li",null,[o(t,{to:"#frontend-knockout"},{default:s(()=>[e("Frontend - Knockout")]),_:1})])])])])]),D,n("p",null,[e("All configuration of the way that Coalesce interacts with your projects, including locating, analyzing, and producing generated code, is done in a json configuration file, "),f,e(". Read more about this file at "),o(a,{to:"/topics/coalesce-json.html"},{default:s(()=>[e("Code Generation Configuration")]),_:1}),e(".")]),v,n("p",null,[e("For each of your "),o(a,{to:"/modeling/model-types/entities.html"},{default:s(()=>[e("Entity Models")]),_:1}),e(", "),o(a,{to:"/modeling/model-types/dtos.html"},{default:s(()=>[e("Custom DTOs")]),_:1}),e(", and "),o(a,{to:"/modeling/model-types/services.html"},{default:s(()=>[e("Services")]),_:1}),e(", an API controller is created in the "),m,e(" directory of your web project. These controllers provide a number of endpoints for interacting with your data.")]),n("p",null,[e("These controllers can be secured at a high level using "),o(a,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:s(()=>[e("Security Attributes")]),_:1}),e(", and when applicable to the type, with "),o(a,{to:"/modeling/model-components/data-sources.html"},{default:s(()=>[e("Data Sources")]),_:1}),e(" and "),o(a,{to:"/modeling/model-components/behaviors.html"},{default:s(()=>[e("Behaviors")]),_:1}),e(".")]),g,n("p",null,[e("For each of your "),o(a,{to:"/modeling/model-types/entities.html"},{default:s(()=>[e("Entity Models")]),_:1}),e(", a C# DTO class is created. These classes are used to hold the data that will be serialized and sent to the client, as well as data that has been received from the client before it has been mapped back to your EF POCO class.")]),n("p",null,[e("See "),o(a,{to:"/stacks/agnostic/dtos.html"},{default:s(()=>[e("Generated C# DTOs")]),_:1}),e(" for more information.")]),b,n("p",null,[e("An overview of the Vue generated code can be found at "),o(a,{to:"/stacks/vue/overview.html"},{default:s(()=>[e("Vue Overview")]),_:1}),e(".")]),C,n("p",null,[e("An overview of the legacy Knockout generated code can be found at "),o(a,{to:"/stacks/ko/overview.html"},{default:s(()=>[e("Knockout Overview")]),_:1}),e(".")])])}const x=c(d,[["render",_],["__file","generation.html.vue"]]);export{x as default}; diff --git a/assets/getting-started-modeling.html.bcc466db.js b/assets/getting-started-modeling.html.bcc466db.js new file mode 100644 index 000000000..bb61440ba --- /dev/null +++ b/assets/getting-started-modeling.html.bcc466db.js @@ -0,0 +1 @@ +import{_ as l,y as d,z as r,W as n,X as e,B as t,Q as a,$ as i,a5 as s,P as c}from"./framework.fe9a73df.js";const u={},p=e("h1",{id:"data-modeling",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#data-modeling","aria-hidden":"true"},"#"),t(" Data Modeling")],-1),h=e("p",null,"At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:",-1),m=e("code",null,"DbSet<>",-1),_=e("code",null,"AppDbContext",-1),g=e("code",null,"ApplicationUser",-1),y=s("
  • Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.

  • Run Coalesce's code generation by either:

    • Running dotnet coalesce in the web project's root directory
    • Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer
  • ",2),f=e("p",null,"You're now at a point where you can start creating your own pages!",-1);function w(b,R){const o=c("RouterLink");return d(),r("div",null,[n(" MARKER:data-modeling "),p,h,e("ul",null,[e("li",null,[e("p",null,[t("Create an initial "),a(o,{to:"/modeling/model-types/entities.html"},{default:i(()=>[t("Data Model")]),_:1}),t(" by adding EF entity classes to the data project and the corresponding "),m,t(" properties to "),_,t(". You will notice that the starter project includes a single model, "),g,t(", to start with. Feel free to change this model or remove it entirely. Read "),a(o,{to:"/modeling/model-types/entities.html"},{default:i(()=>[t("Entity Models")]),_:1}),t(" for more information about creating a data model.")])]),y]),f,n(" MARKER:data-modeling-end ")])}const k=l(u,[["render",w],["__file","getting-started-modeling.html.vue"]]);export{k as default}; diff --git a/assets/getting-started-modeling.html.c1e6e2c4.js b/assets/getting-started-modeling.html.c1e6e2c4.js new file mode 100644 index 000000000..056b53bd7 --- /dev/null +++ b/assets/getting-started-modeling.html.c1e6e2c4.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-ba2d632e","path":"/stacks/agnostic/getting-started-modeling.html","title":"Data Modeling","lang":"en-US","frontmatter":{},"excerpt":"","headers":[],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/agnostic/getting-started-modeling.md"}');export{t as data}; diff --git a/assets/getting-started.html.2295c26a.js b/assets/getting-started.html.2295c26a.js new file mode 100644 index 000000000..678062cfd --- /dev/null +++ b/assets/getting-started.html.2295c26a.js @@ -0,0 +1,56 @@ +import{_ as c,y as r,z as i,X as s,B as n,Q as a,$ as e,a5 as o,P as t}from"./framework.fe9a73df.js";const D={},d=s("h1",{id:"getting-started-with-knockout",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#getting-started-with-knockout","aria-hidden":"true"},"#"),n(" Getting Started with Knockout")],-1),y=s("h2",{id:"creating-a-project",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#creating-a-project","aria-hidden":"true"},"#"),n(" Creating a Project")],-1),u={class:"custom-container warning"},C=s("p",{class:"custom-container-title"},"WARNING",-1),v=o(`

    The quickest and easiest way to create a new Coalesce Knockout application is to use the dotnet new template. In your favorite shell:

    dotnet new install IntelliTect.Coalesce.KnockoutJS.Template
    +dotnet new coalesceko
    +
    `,2),m={href:"https://www.nuget.org/packages/IntelliTect.Coalesce.KnockoutJS.Template/",target:"_blank",rel:"noopener noreferrer"},h=s("img",{src:"https://img.shields.io/nuget/v/IntelliTect.Coalesce.KnockoutJS.Template",alt:""},null,-1),b={href:"https://github.com/IntelliTect/Coalesce.KnockoutJS.Template",target:"_blank",rel:"noopener noreferrer"},g=s("h2",{id:"data-modeling",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#data-modeling","aria-hidden":"true"},"#"),n(" Data Modeling")],-1),E=s("p",null,"At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:",-1),_=s("code",null,"DbSet<>",-1),w=s("code",null,"AppDbContext",-1),f=s("code",null,"ApplicationUser",-1),k=o("
  • Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.

  • Run Coalesce's code generation by either:

    • Running dotnet coalesce in the web project's root directory
    • Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer
  • ",2),A=s("p",null,"You're now at a point where you can start creating your own pages!",-1),F=s("h2",{id:"building-pages-features",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#building-pages-features","aria-hidden":"true"},"#"),n(" Building Pages & Features")],-1),x=s("code",null,"Person",-1),B=s("code",null,"dotnet coalesce",-1),P=o(`
    namespace MyApplication.Data.Models 
    +{
    +    public class Person
    +    {
    +        public int PersonId { get; set; }
    +        public string Name { get; set; }
    +        public DateTimeOffset? BirthDate { get; set; }
    +    }
    +}
    +

    We can create a details page for a Person by creating:

    • A controller in src/MyApplication.Web/Controllers/PersonController.cs:

      namespace MyApplication.Web.Controllers
      +{
      +    public partial class PersonController
      +    {
      +        public IActionResult Details() => View();
      +    }
      +}
      +
    • A view in src/MyApplication.Web/Views/Person/Details.cshtml:

      <h1>Person Details</h1>
      +
      +<div data-bind="with: person">
      +    <dl class="dl-horizontal">
      +        <dt>Name </dt>
      +        <dd data-bind="text: name"></dd>
      +
      +        <dt>Date of Birth </dt>
      +        <dd data-bind="moment: birthDate, format: 'MM/DD/YYYY hh:mm a'"></dd>
      +    </dl>
      +</div>
      +
      +@section Scripts
      +{
      +<script src="~/js/person.details.js"></script>
      +<script>
      +    $(function () {
      +        var vm = new MyApplication.PersonDetails();
      +        ko.applyBindings(vm);
      +        vm.load();
      +    });
      +</script>
      +}
      +
    • And a script in src/MyApplication.Web/Scripts/person.details.ts:

      /// <reference path="viewmodels.generated.d.ts" />
      +
      +module MyApplication {
      +    export class PersonDetails {
      +        public person = new ViewModels.Person();
      +
      +        load() {
      +            var id = Coalesce.Utilities.GetUrlParameter("id");
      +            if (id != null && id != '') {
      +                this.person.load(id);
      +            }
      +        }
      +    }
      +}
      +

    With these pieces in place, we now have a functioning page that will display details about a person. We can start up the application and navigate to /Person/Details?id=1 (assuming a person with ID 1 exists - if not, navigate to /Person/Table and create one).

    `,4);function q(M,T){const l=t("RouterLink"),p=t("ExternalLinkIcon");return r(),i("div",null,[d,y,s("div",u,[C,s("p",null,[n("The Coalesce Knockout.js stack is deprecated and will be receiving only critical bug fixes going forward. You are strongly encouraged to start all new Coalesce projects with "),a(l,{to:"/stacks/vue/getting-started.html"},{default:e(()=>[n("Vue")]),_:1}),n(".")])]),v,s("p",null,[s("a",m,[h,a(p)]),n(" • "),s("a",b,[n("View on GitHub"),a(p)])]),g,E,s("ul",null,[s("li",null,[s("p",null,[n("Create an initial "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[n("Data Model")]),_:1}),n(" by adding EF entity classes to the data project and the corresponding "),_,n(" properties to "),w,n(". You will notice that the starter project includes a single model, "),f,n(", to start with. Feel free to change this model or remove it entirely. Read "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[n("Entity Models")]),_:1}),n(" for more information about creating a data model.")])]),k]),A,F,s("p",null,[n("Lets say we've created a "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[n("model")]),_:1}),n(" called "),x,n(" as follows, and we've ran code generation with "),B,n(":")]),P,s("p",null,[n("From this point, one can start adding more fields, more features, and more flair to the page. Check out all the other documentation in the sidebar to see what else Coalesce has to offer, including the "),a(l,{to:"/stacks/ko/overview.html"},{default:e(()=>[n("Knockout Overview")]),_:1}),n(".")])])}const j=c(D,[["render",q],["__file","getting-started.html.vue"]]);export{j as default}; diff --git a/assets/getting-started.html.435781ec.js b/assets/getting-started.html.435781ec.js new file mode 100644 index 000000000..1679197d1 --- /dev/null +++ b/assets/getting-started.html.435781ec.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-91a9c356","path":"/stacks/vue/getting-started.html","title":"Getting Started with Vue","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Project","slug":"creating-a-project","link":"#creating-a-project","children":[]},{"level":2,"title":"Project Structure","slug":"project-structure","link":"#project-structure","children":[]},{"level":2,"title":"Data Modeling","slug":"data-modeling","link":"#data-modeling","children":[]},{"level":2,"title":"Building Pages & Features","slug":"building-pages-features","link":"#building-pages-features","children":[]}],"git":{"updatedTime":1677795733000},"filePathRelative":"stacks/vue/getting-started.md"}');export{e as data}; diff --git a/assets/getting-started.html.5bf7a7d7.js b/assets/getting-started.html.5bf7a7d7.js new file mode 100644 index 000000000..9c98952f6 --- /dev/null +++ b/assets/getting-started.html.5bf7a7d7.js @@ -0,0 +1,45 @@ +import{_ as c,y as r,z as i,X as n,Q as e,B as s,$ as l,a5 as o,P as p}from"./framework.fe9a73df.js";const D={},d=o(`

    Getting Started with Vue

    Creating a Project

    The quickest and easiest way to create a new Coalesce Vue application is to use the dotnet new template. In your favorite shell:

    mkdir MyCompany.MyProject
    +cd MyCompany.MyProject
    +dotnet new install IntelliTect.Coalesce.Vue.Template
    +dotnet new coalescevue
    +cd *.Web
    +npm ci
    +
    `,4),y={href:"https://www.nuget.org/packages/IntelliTect.Coalesce.Vue.Template/",target:"_blank",rel:"noopener noreferrer"},u=n("img",{src:"https://img.shields.io/nuget/v/IntelliTect.Coalesce.Vue.Template",alt:""},null,-1),h={href:"https://github.com/IntelliTect/Coalesce.Vue.Template",target:"_blank",rel:"noopener noreferrer"},m=n("h2",{id:"project-structure",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#project-structure","aria-hidden":"true"},"#"),s(" Project Structure")],-1),v={class:"custom-container tip"},C=n("p",{class:"custom-container-title"},"Important",-1),g={href:"https://vitejs.dev/",target:"_blank",rel:"noopener noreferrer"},b={href:"https://vitejs.dev/guide/",target:"_blank",rel:"noopener noreferrer"},f=o('

    The structure of the Web project follows the conventions of both ASP.NET Core and Vite. The Vue-specific folders are as follows:

    • /src - Files that should be compiled into your application. CSS/SCSS, TypeScript, Vue SFCs, and so on.
    • /public - Static assets that should be served as files. Includes index.html, the root document of the application.
    • /wwwroot - Target for compiled output.

    During development, no special tooling is required to build your frontend code. Coalesce's UseViteDevelopmentServer in ASP.NET Core will take care of that automatically when the application starts. Just make sure NPM packages have been installed (npm ci).

    Data Modeling

    At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:

    ',5),_=n("code",null,"DbSet<>",-1),w=n("code",null,"AppDbContext",-1),E=n("code",null,"ApplicationUser",-1),k=o("
  • Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.

  • Run Coalesce's code generation by either:

    • Running dotnet coalesce in the web project's root directory
    • Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer
  • ",2),F=n("p",null,"You're now at a point where you can start creating your own pages!",-1),V=n("h2",{id:"building-pages-features",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#building-pages-features","aria-hidden":"true"},"#"),s(" Building Pages & Features")],-1),x=n("code",null,"Person",-1),A=n("code",null,"dotnet coalesce",-1),T=o(`
    namespace MyApplication.Data.Models 
    +{
    +    public class Person
    +    {
    +        public int PersonId { get; set; }
    +        public string Name { get; set; }
    +        public DateTimeOffset? BirthDate { get; set; }
    +    }
    +}
    +
    `,1),S={href:"https://vuejs.org/guide/scaling-up/sfc.html",target:"_blank",rel:"noopener noreferrer"},P=n("code",null,"MyApplication.Web/src/views/person-details.vue",-1),j=o(`
    <template>
    +  <dl>
    +    <dt>Name</dt>
    +    <dd>
    +      <c-display :model="person" for="name" />
    +    </dd>
    +
    +    <dt>Date of Birth</dt>
    +    <dd>
    +      <c-display :model="person" for="birthDate" format="M/d/yyyy" />
    +    </dd>
    +  </dl>
    +</template>
    +
    +<script setup lang="ts"> 
    +import { PersonViewModel } from "@/viewmodels.g";
    +
    +const props = defineProps<{ id: number }>();
    +const person = new PersonViewModel();
    +
    +person.$load(props.id);
    +</script>
    +
    `,1),q={class:"custom-container tip"},I=n("p",{class:"custom-container-title"},"Note",-1),M=n("code",null,"string",-1),B=n("code",null,"number",-1),N=o(`

    We then need to add route to this new view. In MyApplication.Web/src/router.ts, add a new item to the routes array:

    // In the \`routes\` array, add the following item:
    +{
    +  path: '/person/:id',
    +  name: 'person-details',
    +  component: () => import('@/views/person-details.vue'),
    +  props: route => ({ id: +route.params.id }),
    +},
    +

    With these pieces in place, we now have a functioning page that will display details about a person. We can start up the application (or, if it was already running, refresh the page) and navigate to /person/1 (assuming a person with ID 1 exists - if not, navigate to /admin/Person and create one).

    `,3);function R(W,L){const t=p("ExternalLinkIcon"),a=p("RouterLink");return r(),i("div",null,[d,n("p",null,[n("a",y,[u,e(t)]),s(" • "),n("a",h,[s("View on GitHub"),e(t)])]),m,n("div",v,[C,n("p",null,[s("The Vue template is based on "),n("a",g,[s("Vite"),e(t)]),s(". You are strongly encouraged to read through at least the first few pages of the "),n("a",b,[s("Vite Documentation"),e(t)]),s(" before getting started on any development.")])]),f,n("ul",null,[n("li",null,[n("p",null,[s("Create an initial "),e(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[s("Data Model")]),_:1}),s(" by adding EF entity classes to the data project and the corresponding "),_,s(" properties to "),w,s(". You will notice that the starter project includes a single model, "),E,s(", to start with. Feel free to change this model or remove it entirely. Read "),e(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[s("Entity Models")]),_:1}),s(" for more information about creating a data model.")])]),k]),F,V,n("p",null,[s("Lets say we've created a "),e(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[s("model")]),_:1}),s(" called "),x,s(" as follows, and we've ran code generation with "),A,s(":")]),T,n("p",null,[s("We can create a details page for a Person by creating a "),n("a",S,[s("Single File Component"),e(t)]),s(" in "),P,s(":")]),j,n("div",q,[I,n("p",null,[s("In the code above, "),e(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:l(()=>[s("c-display")]),_:1}),s(" is a component that comes from the "),e(a,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:l(()=>[s("Vuetify Components")]),_:1}),s(" for Coalesce.")]),n("p",null,[s("For simple property types like "),M,s(" and "),B,s(" you can always use simple template interpolation syntax, but for more complex properties like dates, "),e(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:l(()=>[s("c-display")]),_:1}),s(" is handy to use because it includes features like built-in date formatting.")])]),N,n("p",null,[s("From this point, you can start adding more fields, more features, and more flair to the page. Check out all the other documentation in the sidebar to see what else Coalesce has to offer, including the "),e(a,{to:"/stacks/vue/overview.html"},{default:l(()=>[s("Vue Overview")]),_:1}),s(".")])])}const G=c(D,[["render",R],["__file","getting-started.html.vue"]]);export{G as default}; diff --git a/assets/getting-started.html.72eb3d61.js b/assets/getting-started.html.72eb3d61.js new file mode 100644 index 000000000..334678cdf --- /dev/null +++ b/assets/getting-started.html.72eb3d61.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-83dc9a7e","path":"/stacks/ko/getting-started.html","title":"Getting Started with Knockout","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Creating a Project","slug":"creating-a-project","link":"#creating-a-project","children":[]},{"level":2,"title":"Data Modeling","slug":"data-modeling","link":"#data-modeling","children":[]},{"level":2,"title":"Building Pages & Features","slug":"building-pages-features","link":"#building-pages-features","children":[]}],"git":{"updatedTime":1672451159000},"filePathRelative":"stacks/ko/getting-started.md"}');export{e as data}; diff --git a/assets/hidden.html.383280bc.js b/assets/hidden.html.383280bc.js new file mode 100644 index 000000000..abc8a9a19 --- /dev/null +++ b/assets/hidden.html.383280bc.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-f75859a2","path":"/modeling/model-components/attributes/hidden.html","title":"[Hidden]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"modeling/model-components/attributes/hidden.md"}');export{e as data}; diff --git a/assets/hidden.html.bc0163d7.js b/assets/hidden.html.bc0163d7.js new file mode 100644 index 000000000..c3a228b1e --- /dev/null +++ b/assets/hidden.html.bc0163d7.js @@ -0,0 +1,8 @@ +import{_ as i,y as r,z as c,X as e,B as s,Q as a,$ as p,a5 as t,P as n}from"./framework.fe9a73df.js";const d={},D=e("h1",{id:"hidden",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#hidden","aria-hidden":"true"},"#"),s(" [Hidden]")],-1),u=e("p",null,"Mark an property as hidden from the edit, List or All areas.",-1),h={class:"custom-container danger"},y=e("p",{class:"custom-container-title"},"DANGER",-1),m=e("p",null,[s("This attribute is "),e("strong",null,"not a security attribute"),s(" - it only affects the rendering of the admin pages. It has no impact on data visibility in the API.")],-1),b=t(`

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    [Hidden(HiddenAttribute.Areas.All)]
    +    public int? IncomeLevelId { get; set; }
    +}
    +

    Properties

    `,3),v=t("

    The areas in which the property should be hidden.

    Enum values are:

    • HiddenAttribute.Areas.None Hide from no generated views. Primary and Foreign keys are hidden by default - setting this value explicitly can override this default behavior.
    • HiddenAttribute.Areas.All Hide from all generated views
    • HiddenAttribute.Areas.List Hide from generated list views only (Knockout Table/Cards, Vue c-admin-table)
    • HiddenAttribute.Areas.Edit Hide from generated editor only (Knockout CreateEdit, Vue c-admin-editor)
    ",3);function _(C,f){const o=n("RouterLink"),l=n("Prop");return r(),c("div",null,[D,u,e("div",h,[y,m,e("p",null,[s("Do not use it to keep certain data private - use the "),a(o,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:p(()=>[s("Security Attributes")]),_:1}),s(" family of attributes for that.")])]),b,a(l,{def:"public Areas Area { get; set; } = Areas.All;",ctor:"1"}),v])}const A=i(d,[["render",_],["__file","hidden.html.vue"]]);export{A as default}; diff --git a/assets/history.html.1b31599a.js b/assets/history.html.1b31599a.js new file mode 100644 index 000000000..2e4a56ef1 --- /dev/null +++ b/assets/history.html.1b31599a.js @@ -0,0 +1 @@ +import{_ as e,y as t,z as i,a5 as a}from"./framework.fe9a73df.js";const o={},s=a('

    Welcome to Coalesce's documentation!

    Coalesce is a framework based on ASP.NET Core that makes rapidly building awesome websites much easier. A project that would take a 3 months to complete now takes 1 month. We built this because we got tired of writing all the boiler plate code that is necessary to make amazing sites.

    It does this by allowing developers to focus on the creative aspects of the solution. The more mundane parts are generated automatically. This means that you get to focus on data modeling, business logic and front-end development. Coalesce does the plumbing.

    Here is a typical workflow

    1. Build an EF Core data model with business logic
    2. Coalesce generates controllers, TypeScript view models, API and view model documentation, and admin pages/examples
    3. Build an interactive and intuitive user experience
    4. Rinse and repeat

    Core Features

    • Built on the latest Microsoft ASP.NET Core
    • Easy to learn
    • TypeScript from the ground up
    • Flexibility to use MVC patterns as required
    • Admin pages for all your models are build automatically and include features like searching, sorting, and paging
    • Robust documentation for the framework
    • Automatically generated documentation for the API layer and TypeScript view models
    • Feature rich TypeScript view models that can be easily extended
    • Many extension points for customizations
    • Abstraction that doesn't require you know how everything works
    • Security and data trimming by role is built in
    • Flexibility about which data to return to the client
    • Open source

    Is Coalesce for Everyone and Every Project?

    Coalesce was designed to create line-of-business applications. It provides a more customizable and maintainable alternative to off the shelf customizable products like SharePoint and Sales Force.

    You should consider using Coalesce if your project:

    • Is small to medium size (1-200 classes)
    • Requires an interactive user experience
    • Has data entry requirements, especially forms, tables, etc.
    • Needs to get started quickly with functional prototypes that can become production software

    Design Decisions and Limitations

    Coalesce is specifically designed to meet the needs of web developers. However, there are lots of ways to do this. We have made a set of decisions which we believe makes for a great development experience

    • ASP.NET Core: there is no intent to back port this to an earlier version
    • EF Core for the object relational mapper
    • Currently uses the full framework because .NET Core doesn't supported the required functionality, yet
    • Knockout for client-side data binding
    • Business logic most easily lives in the model classes
    • Coalesce is designed for relational databases. This might change in the future, but not until we have a compelling use case.

    How Does it Work?

    After you create your classes and the EF data context, Coalesce uses this information to generate code. When the Coalesce CLI (command line interface) is run, the following things happen:

    1. The model is validated to ensure that all the Coalesce specific requirements are met. This includes things like ensuring that all classes have a primary key assigned, validating that linked child objects have a key to their parent, etc. If issues are found, generation stops and the errors are displayed with advise to fix the issues.
    2. The core files needed for Coalesce are copied to the target project. This includes TypeScript base classes, customizable templates, and other files for extension points. Each file is copied twice, once as a file that can be modified in the project and once as an original file. This ensures that if any changes are made by the user these files Coalesce will not overwrite the your changes.
    3. The API controllers are generated. One is generated for each object. This includes methods that get a list of items, get a specific item, save an item, etc.
    4. The TypeScript view models are created. There are two view models for each object. One is a list view model which allows for getting an displaying lists of a type of object. This includes full functionality to sort, filter, search, page, etc. Additionally, a view model that represents the individual object is also created. This has all the properties and methods of the server side object. This is basically a client-side proxy object for representing and manipulating the object on the server side. These objects seamlessly use the API controllers to interact with the server.
    5. Next, the View controller are created. One is create for each model class and provide a tabular view, a card view (for mobile), an editor, and documentation.
    6. Finally, the CSHTML views for the controller are created. These are the actual CSHTML for the above controllers. These not only provide administrative view and editing features, but also serve as an example of how to use the framework

    General Guidance

    Here are a few things we have found helpful when using Coalesce

    • Learn and embrace the Coalesce paradigm and work with it rather than trying to do things another way.
    • Following what we refer to as the 'well worn path' is very helpful. Try to stick to standard ways to do things rather than trying to use esoteric features.
    • Keep your models as consistent and straightforward as possible. Use relational modeling best practices.
    • Remember that public methods on your class models are added to the client side view models and this makes calling business logic from the client really easy.
    • Don't be afraid to fall back to building parts of your site using traditional methods. Coalesce isn't right for everything. But, honestly, we have only done this a few times, like 3.

    The Story

    Why Coalesce

    In 2014 several developers from IntelliTect got together to talk about our craft. There were lots of different backgrounds, but recently we had all been writing web code in C#. We discussed things we enjoyed and things we dreaded. There was an underlying commitment to providing customers with great sites at a reasonable cost. However, those things often seemed at odds because of the complexity of web development.

    The Problem

    For example, writing AJAX drop down lists with type ahead takes quite a bit of plumbing. Layer onto this the need for view models that allow for validation and saving as the user moves from field to field. We absolutely want want to deliver visually pleasing sites with complex UI paradigms. However, all this excellence adds up: complex view models, complex APIs, data binding, ugh.

    Then there is that sinking feeling when you have to add another class to the project knowing that you are going to need to create all this yet again and you consider taking short cuts. Will there really be more than about 20 items in this table, maybe we don't need paging. Inevitably, the customer asks for admin screens. We consider giving them SQL Server Management Console and then consider using the built in ASP.NET list and editor pages. Better sense wins out and we end up spending two weeks building slick admin pages with paging, searching, sorting, etc.

    The Path to the Solution

    That evening we starting talking about the things we loved to do:

    • Data modeling
    • Figuring out and writing business logic
    • Working with customers
    • Making cool user interfaces
    • Creating something new and awesome

    We also lists things that we didn't enjoy

    • Writing the same controller again
    • Creating a view model for a class that is similar but different from another one in the project
    • Putting sorting and paging on every admin page
    • Basically doing anything that feels repetitive or boilerplate

    Over the next few months we talked about this issue, but couldn't find the right abstraction. We talked about other solutions that solve parts of the problem and considered putting together something from several pieces. Nothing felt unified and we ended up with leaky abstractions. We needed some way to divide the problem so that we could build the fun stuff and have something generate the boring stuff. This solution needed to be robust enough to satisfy our customer's needs and also be of use to developers without their needing to know the inner workings of the system.

    Our Solution

    What if we could build the models and business logic and have a tool build everything except the UI? There are great tools like Entity Framework for modeling and good tooling for minimizing duplicate code in user interfaces. And so Coalesce was born, a tool that would bring together the backend and front end development. '

    Coalesce takes Entity Framework Core models and builds controllers, TypeScript view models, and admin pages automatically. These are built in a general way so that they can be applied to many different scenarios. There will always be pages that need to be written by hand and we intentionally don't support many edge cases in order to keep things simple. There is nothing wrong with building something by hand.

    How has it Worked?

    We have been using Coalesce for many of our web projects with great success. Typically, a project is taking about 1/3 the time it was taking before once developers ramp up. The ramp up on Coalesce has typically been a couple of days. We realized that in order for Coalesce to be useful it need to be intuitive to use and easy to understand. We have intentionally used simple paradigms to minimize the learning curve. There are complex bits, but hopefully, those are well hidden and documented as needed.

    ',37),n=[s];function l(r,d){return t(),i("div",null,n)}const c=e(o,[["render",l],["__file","history.html.vue"]]);export{c as default}; diff --git a/assets/history.html.f58a3d69.js b/assets/history.html.f58a3d69.js new file mode 100644 index 000000000..700695fa2 --- /dev/null +++ b/assets/history.html.f58a3d69.js @@ -0,0 +1 @@ +const e=JSON.parse(`{"key":"v-a7d34384","path":"/history.html","title":"Welcome to Coalesce's documentation!","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Core Features","slug":"core-features","link":"#core-features","children":[]},{"level":2,"title":"Is Coalesce for Everyone and Every Project?","slug":"is-coalesce-for-everyone-and-every-project","link":"#is-coalesce-for-everyone-and-every-project","children":[]},{"level":2,"title":"Design Decisions and Limitations","slug":"design-decisions-and-limitations","link":"#design-decisions-and-limitations","children":[]},{"level":2,"title":"How Does it Work?","slug":"how-does-it-work","link":"#how-does-it-work","children":[]},{"level":2,"title":"General Guidance","slug":"general-guidance","link":"#general-guidance","children":[]},{"level":2,"title":"Why Coalesce","slug":"why-coalesce","link":"#why-coalesce","children":[]},{"level":2,"title":"The Problem","slug":"the-problem","link":"#the-problem","children":[]},{"level":2,"title":"The Path to the Solution","slug":"the-path-to-the-solution","link":"#the-path-to-the-solution","children":[]},{"level":2,"title":"Our Solution","slug":"our-solution","link":"#our-solution","children":[]},{"level":2,"title":"How has it Worked?","slug":"how-has-it-worked","link":"#how-has-it-worked","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"history.md"}`);export{e as data}; diff --git a/assets/include-tree.html.5aed11ea.js b/assets/include-tree.html.5aed11ea.js new file mode 100644 index 000000000..fc2d7c4de --- /dev/null +++ b/assets/include-tree.html.5aed11ea.js @@ -0,0 +1,120 @@ +import{_ as r,y as D,z as i,X as s,Q as a,$ as n,B as e,a5 as p,P as t}from"./framework.fe9a73df.js";const y={},d=s("h1",{id:"include-tree",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#include-tree","aria-hidden":"true"},"#"),e(" Include Tree")],-1),u=s("p",null,[e("When Coalesce maps from the your POCO objects that are returned from EF Core queries, it will follow a structure called an "),s("code",null,"IncludeTree"),e(" to determine what relationships to follow and how deep to go in re-creating that structure in the mapped DTOs.")],-1),C={class:"table-of-contents"},m=p(`

    Purpose

    Without an IncludeTree present, Coalesce will map the entire object graph that is reachable from the root object. This can often spiral out of control if there aren't any rules defining how far to go while turning this graph into a tree.

    For example, suppose you had the following model with a many-to-many relationship (key properties omitted for brevity):

    public class Employee
    +{
    +    [ManyToMany("Projects")]
    +    public ICollection<EmployeeProject> EmployeeProjects { get; set; }
    +            
    +    public static IQueryable<Employee> WithProjectsAndMembers(AppDbContext db, ClaimsPrincipal user)
    +    {
    +        // Load all projects of an employee, as well as all members of those projects.
    +        return db.Employees
    +            .Include(e => e.EmployeeProjects)
    +                .ThenInclude(ep => ep.Project.EmployeeProjects)
    +                .ThenInclude(ep => ep.Employee);
    +    }
    +}
    +
    +public class Project
    +{
    +    [ManyToMany("Employees")]
    +    public ICollection<EmployeeProject> EmployeeProjects { get; set; }
    +}
    +
    +public class EmployeeProject
    +{
    +    public Employee Employee { get; set; }
    +    public Project Project { get; set; }
    +}
    +

    Now, imagine that you have five employees and five projects, with every employee being a member of every project (i.e. there are 25 EmployeeProject rows).

    Your client code makes a call to the Coalesce-generated API to load Employee #1 using the custom data source:

    `,6),h=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"Employee"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"EmployeeViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"EmployeeViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"Employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"DataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"WithProjectsAndMembers"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),E=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Employee"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSource"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"dataSources"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"WithProjectsAndMembers"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"employee"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),v=s("p",null,[e("If you're already familiar with the fact that an "),s("code",null,"IncludeTree"),e(" is implicitly created in this scenario, then imagine for a moment that this is not the case (if you're not familiar with this fact, then keep reading!).")],-1),b=s("code",null,"DbContext",-1),F=p(`

    To map these objects to DTOs, we start with the root (employee #1) and expand outward from there until the entire object graph has been faithfully re-created with DTO objects, including all navigation properties.

    The root DTO object (employee #1) then eventually is passed to the JSON serializer by ASP.NET Core to formulate the response to the request. As the object is serialized to JSON, the only objects that are not serialized are those that were already serialized as an ancestor of itself. What this ultimately means is that the structure of the serialized JSON with our example scenario ends up following a pattern like this (the vast majority of items have been omitted):

    Employee#1
    +    EmployeeProject#1
    +        Project#1
    +            EmployeeProject#6
    +                Employee#2
    +                    EmployeeProject#7
    +                        Project#2
    +                            ... continues down through all remaining employees and projects.
    +                    ...
    +            EmployeeProject#11
    +                Employee#3
    +            ...
    +    EmployeeProject#2
    +        Project#2
    +    ...
    +

    See how the structure includes the EmployeeProjects of Employee#2? We didn't write our custom data source calls to .Include in such a way that indicated that we wanted the root employee, their projects, the employees of those projects, and then the projects of those employees. But, because the JSON serializer blindly follows the object graph, that's what gets serialized. It turns out that the depth of the tree increases on the order of O(n^2), and the total size increases on the order of Ω(n!).

    This is where IncludeTree comes in. When you use a custom data source like we did above, Coalesce automatically captures the structure of the calls to .Include and .ThenInclude, and uses this to perform trimming during creation of the DTO objects.

    With an IncludeTree in place, our new serialized structure looks like this:

    Employee#1
    +    EmployeeProject#1
    +        Project#1
    +            EmployeeProject#6
    +                Employee#2
    +            EmployeeProject#11
    +                Employee#3
    +            ...
    +    EmployeeProject#2
    +        Project#2
    +    ...
    +

    No more extra data trailing off the end of the projects' employees!

    Usage

    Custom Data Sources

    `,10),g=s("code",null,"IncludeTree",-1),f=s("code",null,".Include",-1),_=s("code",null,".ThenInclude",-1),A=s("code",null,"IncludeTree",-1),w=p(`

    However, there are sometimes cases where you perform complex loading in these methods that involves loading data into the current DbContext outside of the IQueryable that is returned from the method. The most common situation for this is needing to conditionally load related data - for example, load all children of an object where the child has a certain value of a Status property.

    In these cases, Coalesce provides a pair of extension methods, .IncludedSeparately and .ThenIncluded, that can be used to merge in the structure of the data that was loaded separately from the main IQueryable.

    For example:

    public override IQueryable<Employee> GetQuery()
    +{
    +    // Load all projects that are complete, and their members, into the db context.
    +    Db.Projects
    +        .Include(p => p.EmployeeProjects).ThenInclude(ep => ep.Employee)
    +        .Where(p => p.Status == ProjectStatus.Complete)
    +        .Load();
    +
    +    // Return an employee query, and notify Coalesce that we loaded the projects in a different query.
    +    return Db.Employees
    +        .IncludedSeparately(e => e.EmployeeProjects)
    +        .ThenIncluded(ep => ep.Project.EmployeeProjects)
    +        .ThenIncluded(ep => ep.Employee);
    +}
    +
    `,4),I=s("code",null,"GetIncludeTree",-1),j=p(`
    public override IncludeTree GetIncludeTree(IQueryable<T> query, IDataSourceParameters parameters) => Db
    +    .Employees
    +    .IncludedSeparately(e => e.EmployeeProjects)
    +    .ThenIncluded(ep => ep.Project.EmployeeProjects)
    +    .ThenIncluded(ep => ep.Employee)
    +    .GetIncludeTree(); 
    +

    Model Methods

    `,2),T=s("code",null,"IncludeTree",-1),x=s("code",null,"IncludeTree",-1),P={class:"custom-container tip"},B=s("p",{class:"custom-container-title"},"TIP",-1),k=s("p",null,[e("An "),s("code",null,"IncludeTree"),e(" can be obtained from any "),s("code",null,"IQueryable"),e(" by calling the "),s("code",null,"GetIncludeTree"),e(" extension method ("),s("code",null,"using IntelliTect.Coalesce.Helpers.IncludeTree"),e(").")],-1),S=s("code",null,"DbContext",-1),O=s("code",null,"Enumerable.Empty().AsQueryable()",-1),M=s("code",null,"IQueryable",-1),q=s("strong",null,"must",-1),z=s("code",null,"IncludedSeparately",-1),N=s("code",null,"Include",-1),Q=s("code",null,"DbSet",-1),W=p(`

    See the following two techniques for returning an IncludeTree from a custom method:

    ItemResult.IncludeTree

    The easiest and most versatile way to return an IncludeTree from a custom method is to make that method return an ItemResult<T>, and then set the IncludeTree property of the ItemResult object. For example:

    public class Employee
    +{
    +    public async Task<ItemResult<ICollection<Employee>>> GetChainOfCommand(AppDbContext db)
    +    {
    +        IQueryable<Employee> query = db.Employees
    +            .Include(e => e.Supervisor);
    +
    +        var ret = new List<Employee>();
    +        var current = this;
    +        while (current.Supervisor != null)
    +        {
    +            ret.Push(current);
    +            current = await query.FirstOrDefaultAsync(e => e.EmployeeId == current.SupervisorId);
    +        }
    +
    +        return new(ret, includeTree: query.GetIncludeTree());
    +    }
    +}
    +

    Out Parameter

    To tell Coalesce about the structure of the data returned from a model method, you can also add out IncludeTree includeTree to the signature of the method. Inside your method, set includeTree to an instance of an IncludeTree. However, this approach cannot be used on async methods, since out parameters are not allowed on async methods in C#. For example:

    public class Employee
    +{
    +    public ICollection<Employee> GetChainOfCommand(AppDbContext db, out IncludeTree includeTree)
    +    {
    +        IQueryable<Employee> query = db.Employees
    +            .Include(e => e.Supervisor);
    +
    +        var ret = new List<Employee>();
    +        var current = this;
    +        while (current.Supervisor != null)
    +        {
    +            ret.Push(current);
    +            current = query.FirstOrDefault(e => e.EmployeeId == current.SupervisorId);
    +        }
    +
    +        includeTree = query.GetIncludeTree();
    +
    +        return ret;
    +    }
    +}
    +

    External Type Caveats

    `,8),G=s("code",null,"IncludeTree",-1),L=s("p",null,"By not filtering unmapped properties, you as the developer don't need to account for them in every place throughout your application where they appear - instead, they 'just work' and show up on the client as expected.",-1),R=s("p",null,[e("Note also that this statement does not apply to database-mapped objects that hang off of unmapped objects - any time a database-mapped object appears, it will be controlled by your include tree. If no include tree is present (because nothing was specified for the unmapped property), these mapped objects hanging off of unmapped objects will be serialized freely and with all circular references, unless you include some calls to "),s("code",null,".IncludedSeparately(m => m.MyUnmappedProperty.MyMappedProperty)"),e(" to limit those objects down.")],-1);function V(J,H){const o=t("router-link"),c=t("CodeTabs"),l=t("RouterLink");return D(),i("div",null,[d,u,s("nav",C,[s("ul",null,[s("li",null,[a(o,{to:"#purpose"},{default:n(()=>[e("Purpose")]),_:1})]),s("li",null,[a(o,{to:"#usage"},{default:n(()=>[e("Usage")]),_:1}),s("ul",null,[s("li",null,[a(o,{to:"#custom-data-sources"},{default:n(()=>[e("Custom Data Sources")]),_:1})]),s("li",null,[a(o,{to:"#model-methods"},{default:n(()=>[e("Model Methods")]),_:1})]),s("li",null,[a(o,{to:"#external-type-caveats"},{default:n(()=>[e("External Type Caveats")]),_:1})])])])])]),m,a(c,null,{vue:n(()=>[h]),knockout:n(()=>[E]),_:1}),v,s("p",null,[e("After Coalesce has called your "),a(l,{to:"/modeling/model-components/data-sources.html"},{default:n(()=>[e("Data Sources")]),_:1}),e(" and evaluated the EF IQueryable returned, there are now 35 objects loaded into the current "),b,e(" being used to handle this request - the 5 employees, 5 projects, and 25 relationships.")]),F,s("p",null,[e("In most cases, you don't have to worry about creating an "),g,e(". When using the "),a(l,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:n(()=>[e("Standard Data Source")]),_:1}),e(" (or a derivative), the structure of the "),f,e(" and "),_,e(" calls will be captured automatically and be turned into an "),A,e(".")]),w,s("p",null,[e("You can also override the "),I,e(" method of the "),a(l,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:n(()=>[e("Standard Data Source")]),_:1}),e(" to achieve the same result:")]),j,s("p",null,[e("If you have "),a(l,{to:"/modeling/model-components/methods.html"},{default:n(()=>[e("custom methods")]),_:1}),e(" that return object data, you may also want to control the structure of the returned data when it is serialized. Fortunately, you can also use "),T,e(" in these situations. Without an "),x,e(", the entire object graph is traversed and serialized without limit.")]),s("div",P,[B,k,s("p",null,[e("In situations where your root object isn't on your "),S,e(" (see "),a(l,{to:"/modeling/model-types/external-types.html"},{default:n(()=>[e("External Types")]),_:1}),e("), you can use "),O,e(" to get an "),M,e(" to start from. When you do this, you "),q,e(" use "),z,e(" - the regular EF "),N,e(" method won't work without a "),Q,e(".")])]),W,s("p",null,[e("One important point remains regarding "),G,e(" - it is not used to control the serialization of objects which are not mapped to the database, known as "),a(l,{to:"/modeling/model-types/external-types.html"},{default:n(()=>[e("External Types")]),_:1}),e(". External Types are always put into the DTOs when encountered (unless otherwise prevented by "),a(l,{to:"/modeling/model-components/attributes/dto-includes-excludes.html"},{default:n(()=>[e("[DtoIncludes] & [DtoExcludes]")]),_:1}),e(" or "),a(l,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:n(()=>[e("Security Attributes")]),_:1}),e("), with the assumption that because these objects are created by you (as opposed to Entity Framework), you are responsible for preventing any undesired circular references.")]),L,R])}const $=r(y,[["render",V],["__file","include-tree.html.vue"]]);export{$ as default}; diff --git a/assets/include-tree.html.78a40435.js b/assets/include-tree.html.78a40435.js new file mode 100644 index 000000000..ae3f2ec78 --- /dev/null +++ b/assets/include-tree.html.78a40435.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5ee46ccb","path":"/concepts/include-tree.html","title":"Include Tree","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Purpose","slug":"purpose","link":"#purpose","children":[]},{"level":2,"title":"Usage","slug":"usage","link":"#usage","children":[{"level":3,"title":"Custom Data Sources","slug":"custom-data-sources","link":"#custom-data-sources","children":[]},{"level":3,"title":"Model Methods","slug":"model-methods","link":"#model-methods","children":[]},{"level":3,"title":"External Type Caveats","slug":"external-type-caveats","link":"#external-type-caveats","children":[]}]}],"git":{"updatedTime":1664236724000},"filePathRelative":"concepts/include-tree.md"}');export{e as data}; diff --git a/assets/includes.html.08fa3b0f.js b/assets/includes.html.08fa3b0f.js new file mode 100644 index 000000000..2626761c7 --- /dev/null +++ b/assets/includes.html.08fa3b0f.js @@ -0,0 +1,55 @@ +import{_ as p,y as D,z as d,X as s,Q as n,$ as l,B as e,a5 as r,P as t}from"./framework.fe9a73df.js";const u={},y=s("h1",{id:"includes-string",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#includes-string","aria-hidden":"true"},"#"),e(" Includes String")],-1),C=s("p",null,'Coalesce provides a number of extension points for loading & serialization which make use of a concept called an "includes string" (also referred to as "include string" or just "includes").',-1),h={class:"table-of-contents"},m=s("h2",{id:"includes-string-1",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#includes-string-1","aria-hidden":"true"},"#"),e(" Includes String")],-1),b=s("p",null,"The includes string is simply a string which can be set to any arbitrary value. It is passed from the client to the server in order to control data loading and serialization. It can be set on both the TypeScript ViewModels and the ListViewModels.",-1),v=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},", "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),_=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"person"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),E=s("p",null,"The default value (i.e. no action) is the empty string.",-1),g=s("h3",{id:"special-values",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#special-values","aria-hidden":"true"},"#"),e(" Special Values")],-1),w=s("p",null,[e("There are a few values of "),s("code",null,"includes"),e(" that are either set by default in the auto-generated views, or otherwise have special meaning:")],-1),f=s("thead",null,[s("tr",null,[s("th",null,"Value"),s("th",null,"Description")])],-1),F=s("td",null,[s("code",null,"'none'")],-1),L=s("code",null,"includes",-1),A=s("code",null,"none",-1),x=s("td",null,[s("code",null,"'admin-list'")],-1),k=s("td",null,[s("code",null,"'admin-editor'")],-1),P=s("tr",null,[s("td",null,[s("code",null,"'Editor'")]),s("td",null,"Used when loading an object in the generated Knockout CreateEdit views.")],-1),V=s("tr",null,[s("td",null,[s("code",null,"'ListGen'")]),s("td",null,[e("Used when loading a list of objects in the generated Knockout Table and Cards views. For example, "),s("code",null,"PersonListGen")])],-1),B=s("h2",{id:"dtoincludes-dtoexcludes",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#dtoincludes-dtoexcludes","aria-hidden":"true"},"#"),e(" DtoIncludes & DtoExcludes")],-1),I=s("p",null,[e("There are two C# attributes, "),s("code",null,"DtoIncludes"),e(" and "),s("code",null,"DtoExcludes"),e(", that can be used to annotate your data model in order to control what data gets put into the DTOs and ultimately serialized to JSON and sent out to the client.")],-1),j=s("p",null,[e("When the database entries are returned to the client they will be trimmed based on the requested includes string and the values in "),s("code",null,"DtoExcludes"),e(" and "),s("code",null,"DtoIncludes"),e(".")],-1),M={class:"custom-container danger"},S=s("p",{class:"custom-container-title"},"DANGER",-1),T=s("p",null,[e("These attributes are "),s("strong",null,"not security attributes"),e(" - consumers of your application's API can set the includes string to any value when making a request.")],-1),q=s("p",null,[e("It is important to note that the value of the includes string will match against these attributes on "),s("em",null,"any"),e(" of your models that appears in the object graph being mapped to DTOs - it is not limited only to the model type of the root object.")],-1),N={class:"custom-container tip"},$=s("p",{class:"custom-container-title"},"Important",-1),U=s("code",null,"DtoIncludes",-1),z=s("em",null,"permits",-1),O=s("em",null,"already",-1),G=r(`

    Example Usage

    Server code:

    public class Person
    +{
    +    // Don't include CreatedBy when editing - will be included for all other views
    +    [DtoExcludes("Editor")]
    +    public AppUser CreatedBy { get; set; }
    +
    +    // Only include the Person's Department when \`includes == "details"\` on the TypeScript ViewModel.
    +    [DtoIncludes("details")]
    +    public Department Department { get; set; }
    +
    +    // LastName will be included in all views
    +    public string LastName { get; set; }
    +}
    +
    +public class Department
    +{
    +    [DtoIncludes("details")]
    +    public ICollection<Person> People { get; set; }
    +}
    +

    Client code:

    `,4),R=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#4FC1FF"}},"personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList.$items will not contain CreatedBy nor Department objects.")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"const"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#4FC1FF"}},"personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"$includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. ")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#6A9955"}},"// Department will be allowed to include its other Person objects.")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),K=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"Editor"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#6A9955"}},"// objects in personList.items will not contain CreatedBy nor Department objects.")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ListViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"PersonList"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"includes"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#CE9178"}},'"details"'),s("span",{style:{color:"#D4D4D4"}},";")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"personList2"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"(() "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#6A9955"}},"// objects in personList2.items will be allowed to contain both CreatedBy and Department objects. Department will be allowed to include its other Person objects.")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),J=s("h3",{id:"properties",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#properties","aria-hidden":"true"},"#"),e(" Properties")],-1),Q=s("code",null,"includes",-1),W=r("

    For DtoIncludes, this will be the values of includes for which this property will be allowed to be serialized and sent to the client.

    For DtoExcludes, this will be the values of includes for which this property will not be serialized and sent to the client.

    ",2);function X(H,Y){const a=t("router-link"),c=t("CodeTabs"),o=t("RouterLink"),i=t("Prop");return D(),d("div",null,[y,C,s("nav",h,[s("ul",null,[s("li",null,[n(a,{to:"#includes-string-1"},{default:l(()=>[e("Includes String")]),_:1}),s("ul",null,[s("li",null,[n(a,{to:"#special-values"},{default:l(()=>[e("Special Values")]),_:1})])])]),s("li",null,[n(a,{to:"#dtoincludes-dtoexcludes"},{default:l(()=>[e("DtoIncludes & DtoExcludes")]),_:1}),s("ul",null,[s("li",null,[n(a,{to:"#example-usage"},{default:l(()=>[e("Example Usage")]),_:1})]),s("li",null,[n(a,{to:"#properties"},{default:l(()=>[e("Properties")]),_:1})])])])])]),m,b,n(c,null,{vue:l(()=>[v]),knockout:l(()=>[_]),_:1}),E,g,w,s("table",null,[f,s("tbody",null,[s("tr",null,[F,s("td",null,[e("Setting "),L,e(" to "),A,e(" suppresses the "),n(o,{to:"/modeling/model-components/data-sources.html#default-loading-behavior"},{default:l(()=>[e("Default Loading Behavior")]),_:1}),e(" provided by the "),n(o,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[e("Standard Data Source")]),_:1}),e(" - The resulting data will be the requested object (or list of objects) and nothing more.")])]),s("tr",null,[x,s("td",null,[e("Used when loading a list of objects in the "),n(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html"},{default:l(()=>[e("Vue admin list page")]),_:1}),e(".")])]),s("tr",null,[k,s("td",null,[e("Used when loading an object in the "),n(o,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html"},{default:l(()=>[e("Vue admin editor component")]),_:1}),e(".")])]),P,V])]),B,s("p",null,[e("Main document: "),n(o,{to:"/modeling/model-components/attributes/dto-includes-excludes.html"},{default:l(()=>[e("[DtoIncludes] & [DtoExcludes]")]),_:1}),e(".")]),I,j,s("div",M,[S,T,s("p",null,[e("Do not use them to keep certain data private - use the "),n(o,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:l(()=>[e("Security Attributes")]),_:1}),e(" family of attributes for that.")])]),q,s("div",N,[$,s("p",null,[U,e(" does not ensure that specific data will be loaded from the database. It only "),z,e(" what is "),O,e(" loaded into the current EF DbContext to be returned from the API. See "),n(o,{to:"/modeling/model-components/data-sources.html"},{default:l(()=>[e("Data Sources")]),_:1}),e(" to learn how to control what data gets loaded from the database.")])]),G,n(c,null,{vue:l(()=>[R]),knockout:l(()=>[K]),_:1}),J,n(i,{def:"public string ContentViews { get; set; }",ctor:"1"}),e(),s("p",null,[e("A comma-delimited list of values of "),n(o,{to:"/concepts/includes.html"},{default:l(()=>[Q]),_:1}),e(" on which to operate.")]),W])}const ss=p(u,[["render",X],["__file","includes.html.vue"]]);export{ss as default}; diff --git a/assets/includes.html.5136332d.js b/assets/includes.html.5136332d.js new file mode 100644 index 000000000..52e3642f5 --- /dev/null +++ b/assets/includes.html.5136332d.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-5d077123","path":"/concepts/includes.html","title":"Includes String","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Includes String","slug":"includes-string-1","link":"#includes-string-1","children":[{"level":3,"title":"Special Values","slug":"special-values","link":"#special-values","children":[]}]},{"level":2,"title":"DtoIncludes & DtoExcludes","slug":"dtoincludes-dtoexcludes","link":"#dtoincludes-dtoexcludes","children":[{"level":3,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":3,"title":"Properties","slug":"properties","link":"#properties","children":[]}]}],"git":{"updatedTime":1687826807000},"filePathRelative":"concepts/includes.md"}');export{e as data}; diff --git a/assets/index.esm.dea45d94.js b/assets/index.esm.dea45d94.js new file mode 100644 index 000000000..c7ce1c8e9 --- /dev/null +++ b/assets/index.esm.dea45d94.js @@ -0,0 +1,7 @@ +var Ge=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},ve={exports:{}};(function(h,b){(function(j,n){h.exports=n()})(Ge,function(){return j={770:function(S,L,y){var E=this&&this.__importDefault||function(l){return l&&l.__esModule?l:{default:l}};Object.defineProperty(L,"__esModule",{value:!0}),L.setDefaultDebugCall=L.createOnigScanner=L.createOnigString=L.loadWASM=L.OnigScanner=L.OnigString=void 0;const N=E(y(418));let r=null,d=!1;class k{constructor(u){const p=u.length,o=k._utf8ByteLength(u),c=o!==p,e=c?new Uint32Array(p+1):null;c&&(e[p]=o);const t=c?new Uint32Array(o+1):null;c&&(t[o]=p);const s=new Uint8Array(o);let _=0;for(let v=0;v=55296&&P<=56319&&v+1=56320&&q<=57343&&(A=65536+(P-55296<<10)|q-56320,I=!0)}c&&(e[v]=_,I&&(e[v+1]=_),A<=127?t[_+0]=v:A<=2047?(t[_+0]=v,t[_+1]=v):A<=65535?(t[_+0]=v,t[_+1]=v,t[_+2]=v):(t[_+0]=v,t[_+1]=v,t[_+2]=v,t[_+3]=v)),A<=127?s[_++]=A:A<=2047?(s[_++]=192|(1984&A)>>>6,s[_++]=128|(63&A)>>>0):A<=65535?(s[_++]=224|(61440&A)>>>12,s[_++]=128|(4032&A)>>>6,s[_++]=128|(63&A)>>>0):(s[_++]=240|(1835008&A)>>>18,s[_++]=128|(258048&A)>>>12,s[_++]=128|(4032&A)>>>6,s[_++]=128|(63&A)>>>0),I&&v++}this.utf16Length=p,this.utf8Length=o,this.utf16Value=u,this.utf8Value=s,this.utf16OffsetToUtf8=e,this.utf8OffsetToUtf16=t}static _utf8ByteLength(u){let p=0;for(let o=0,c=u.length;o=55296&&e<=56319&&o+1=56320&&_<=57343&&(t=65536+(e-55296<<10)|_-56320,s=!0)}p+=t<=127?1:t<=2047?2:t<=65535?3:4,s&&o++}return p}createString(u){const p=u._omalloc(this.utf8Length);return u.HEAPU8.set(this.utf8Value,p),p}}class w{constructor(u){if(this.id=++w.LAST_ID,!r)throw new Error("Must invoke loadWASM first.");this._onigBinding=r,this.content=u;const p=new k(u);this.utf16Length=p.utf16Length,this.utf8Length=p.utf8Length,this.utf16OffsetToUtf8=p.utf16OffsetToUtf8,this.utf8OffsetToUtf16=p.utf8OffsetToUtf16,this.utf8Length<1e4&&!w._sharedPtrInUse?(w._sharedPtr||(w._sharedPtr=r._omalloc(1e4)),w._sharedPtrInUse=!0,r.HEAPU8.set(p.utf8Value,w._sharedPtr),this.ptr=w._sharedPtr):this.ptr=p.createString(r)}convertUtf8OffsetToUtf16(u){return this.utf8OffsetToUtf16?u<0?0:u>this.utf8Length?this.utf16Length:this.utf8OffsetToUtf16[u]:u}convertUtf16OffsetToUtf8(u){return this.utf16OffsetToUtf8?u<0?0:u>this.utf16Length?this.utf8Length:this.utf16OffsetToUtf8[u]:u}dispose(){this.ptr===w._sharedPtr?w._sharedPtrInUse=!1:this._onigBinding._ofree(this.ptr)}}L.OnigString=w,w.LAST_ID=0,w._sharedPtr=0,w._sharedPtrInUse=!1;class i{constructor(u){if(!r)throw new Error("Must invoke loadWASM first.");const p=[],o=[];for(let s=0,_=u.length;s<_;s++){const v=new k(u[s]);p[s]=v.createString(r),o[s]=v.utf8Length}const c=r._omalloc(4*u.length);r.HEAPU32.set(p,c/4);const e=r._omalloc(4*u.length);r.HEAPU32.set(o,e/4);const t=r._createOnigScanner(c,e,u.length);for(let s=0,_=u.length;s<_;s++)r._ofree(p[s]);r._ofree(e),r._ofree(c),t===0&&function(s){throw new Error(s.UTF8ToString(s._getLastOnigError()))}(r),this._onigBinding=r,this._ptr=t}dispose(){this._onigBinding._freeOnigScanner(this._ptr)}findNextMatchSync(u,p,o){let c=d,e=0;if(typeof o=="number"?(8&o&&(c=!0),e=o):typeof o=="boolean"&&(c=o),typeof u=="string"){u=new w(u);const t=this._findNextMatchSync(u,p,c,e);return u.dispose(),t}return this._findNextMatchSync(u,p,c,e)}_findNextMatchSync(u,p,o,c){const e=this._onigBinding;let t;if(t=o?e._findNextOnigScannerMatchDbg(this._ptr,u.id,u.ptr,u.utf8Length,u.convertUtf16OffsetToUtf8(p),c):e._findNextOnigScannerMatch(this._ptr,u.id,u.ptr,u.utf8Length,u.convertUtf16OffsetToUtf8(p),c),t===0)return null;const s=e.HEAPU32;let _=t/4;const v=s[_++],P=s[_++];let A=[];for(let I=0;IWebAssembly.instantiateStreaming(t,s)}(e):function(t){return async s=>{const _=await t.arrayBuffer();return WebAssembly.instantiate(_,s)}}(e):function(t){return s=>WebAssembly.instantiate(t,s)}(e)}return g=new Promise((e,t)=>{o=e,c=t}),function(e,t,s,_){N.default({print:t,instantiateWasm:(v,P)=>{if(typeof performance>"u"){const A=()=>Date.now();v.env.emscripten_get_now=A,v.wasi_snapshot_preview1.emscripten_get_now=A}return e(v).then(A=>P(A.instance),_),{}}}).then(v=>{r=v,s()})}(u,p,o,c),g},L.createOnigString=function(l){return new w(l)},L.createOnigScanner=function(l){return new i(l)},L.setDefaultDebugCall=function(l){d=l}},418:S=>{var L=(typeof document<"u"&&document.currentScript&&document.currentScript.src,function(y){var E,N,r=(y=y||{})!==void 0?y:{};r.ready=new Promise(function(O,W){E=O,N=W});var d,k={};for(d in r)r.hasOwnProperty(d)&&(k[d]=r[d]);var w,i=!1,T="";function g(O){return r.locateFile?r.locateFile(O,T):T+O}w=function(O){var W;return typeof readbuffer=="function"?new Uint8Array(readbuffer(O)):(e(typeof(W=read(O,"binary"))=="object"),W)},typeof scriptArgs<"u"&&scriptArgs,typeof onig_print<"u"&&(typeof console>"u"&&(console={}),console.log=onig_print,console.warn=console.error=typeof printErr<"u"?printErr:onig_print);var l=r.print||console.log.bind(console),u=r.printErr||console.warn.bind(console);for(d in k)k.hasOwnProperty(d)&&(r[d]=k[d]);k=null,r.arguments&&r.arguments,r.thisProgram&&r.thisProgram,r.quit&&r.quit;var p,o;r.wasmBinary&&(p=r.wasmBinary),r.noExitRuntime,typeof WebAssembly!="object"&&$("no native wasm support detected");var c=!1;function e(O,W){O||$("Assertion failed: "+W)}var t,s,_,v=typeof TextDecoder<"u"?new TextDecoder("utf8"):void 0;function P(O,W,ne){for(var fe=W+ne,ee=W;O[ee]&&!(ee>=fe);)++ee;if(ee-W>16&&O.subarray&&v)return v.decode(O.subarray(W,ee));for(var oe="";W>10,56320|1023&Oe)}}else oe+=String.fromCharCode((31&ce)<<6|me)}else oe+=String.fromCharCode(ce)}return oe}function A(O,W){return O?P(s,O,W):""}function I(O,W){return O%W>0&&(O+=W-O%W),O}function q(O){t=O,r.HEAP8=new Int8Array(O),r.HEAP16=new Int16Array(O),r.HEAP32=_=new Int32Array(O),r.HEAPU8=s=new Uint8Array(O),r.HEAPU16=new Uint16Array(O),r.HEAPU32=new Uint32Array(O),r.HEAPF32=new Float32Array(O),r.HEAPF64=new Float64Array(O)}typeof TextDecoder<"u"&&new TextDecoder("utf-16le"),r.INITIAL_MEMORY;var H,K=[],Z=[],he=[],f=[];function a(){if(r.preRun)for(typeof r.preRun=="function"&&(r.preRun=[r.preRun]);r.preRun.length;)M(r.preRun.shift());Y(K)}function m(){Y(Z)}function x(){Y(he)}function C(){if(r.postRun)for(typeof r.postRun=="function"&&(r.postRun=[r.postRun]);r.postRun.length;)F(r.postRun.shift());Y(f)}function M(O){K.unshift(O)}function F(O){f.unshift(O)}Z.push({func:function(){Ie()}});var G=0,B=null;function D(O){G++,r.monitorRunDependencies&&r.monitorRunDependencies(G)}function z(O){if(G--,r.monitorRunDependencies&&r.monitorRunDependencies(G),G==0&&B){var W=B;B=null,W()}}function $(O){r.onAbort&&r.onAbort(O),u(O+=""),c=!0,O="abort("+O+"). Build with -s ASSERTIONS=1 for more info.";var W=new WebAssembly.RuntimeError(O);throw N(W),W}function R(O,W){return String.prototype.startsWith?O.startsWith(W):O.indexOf(W)===0}r.preloadedImages={},r.preloadedAudios={};var U="data:application/octet-stream;base64,";function V(O){return R(O,U)}var X,Q="onig.wasm";function re(O){try{if(O==Q&&p)return new Uint8Array(p);if(w)return w(O);throw"both async and sync fetching of the wasm failed"}catch(W){$(W)}}function te(){return p||!i||typeof fetch!="function"?Promise.resolve().then(function(){return re(Q)}):fetch(Q,{credentials:"same-origin"}).then(function(O){if(!O.ok)throw"failed to load wasm binary file at '"+Q+"'";return O.arrayBuffer()}).catch(function(){return re(Q)})}function se(){var O={env:Re,wasi_snapshot_preview1:Re};function W(ee,oe){var ce=ee.exports;r.asm=ce,q((o=r.asm.memory).buffer),H=r.asm.__indirect_function_table,z()}function ne(ee){W(ee.instance)}function fe(ee){return te().then(function(oe){return WebAssembly.instantiate(oe,O)}).then(ee,function(oe){u("failed to asynchronously prepare wasm: "+oe),$(oe)})}if(D(),r.instantiateWasm)try{return r.instantiateWasm(O,W)}catch(ee){return u("Module.instantiateWasm callback failed with error: "+ee),!1}return(p||typeof WebAssembly.instantiateStreaming!="function"||V(Q)||typeof fetch!="function"?fe(ne):fetch(Q,{credentials:"same-origin"}).then(function(ee){return WebAssembly.instantiateStreaming(ee,O).then(ne,function(oe){return u("wasm streaming compile failed: "+oe),u("falling back to ArrayBuffer instantiation"),fe(ne)})})).catch(N),{}}function Y(O){for(;O.length>0;){var W=O.shift();if(typeof W!="function"){var ne=W.func;typeof ne=="number"?W.arg===void 0?H.get(ne)():H.get(ne)(W.arg):ne(W.arg===void 0?null:W.arg)}else W(r)}}function ie(O,W,ne){s.copyWithin(O,W,W+ne)}function J(){return s.length}function ue(O){try{return o.grow(O-t.byteLength+65535>>>16),q(o.buffer),1}catch{}}function ae(O){var W=J(),ne=2147483648;if(O>ne)return!1;for(var fe=1;fe<=4;fe*=2){var ee=W*(1+.2/fe);if(ee=Math.min(ee,O+100663296),ue(Math.min(ne,I(Math.max(O,ee),65536))))return!0}return!1}V(Q)||(Q=g(Q)),X=typeof dateNow<"u"?dateNow:function(){return performance.now()};var pe={mappings:{},buffers:[null,[],[]],printChar:function(O,W){var ne=pe.buffers[O];W===0||W===10?((O===1?l:u)(P(ne,0)),ne.length=0):ne.push(W)},varargs:void 0,get:function(){return pe.varargs+=4,_[pe.varargs-4>>2]},getStr:function(O){return A(O)},get64:function(O,W){return O}};function ke(O,W,ne,fe){for(var ee=0,oe=0;oe>2],me=_[W+(8*oe+4)>>2],_e=0;_e>2]=ee,0}function ge(O){}var de,Re={emscripten_get_now:X,emscripten_memcpy_big:ie,emscripten_resize_heap:ae,fd_write:ke,setTempRet0:ge},Ie=(se(),r.___wasm_call_ctors=function(){return(Ie=r.___wasm_call_ctors=r.asm.__wasm_call_ctors).apply(null,arguments)});function je(O){function W(){de||(de=!0,r.calledRun=!0,c||(m(),x(),E(r),r.onRuntimeInitialized&&r.onRuntimeInitialized(),C()))}G>0||(a(),G>0||(r.setStatus?(r.setStatus("Running..."),setTimeout(function(){setTimeout(function(){r.setStatus("")},1),W()},1)):W()))}if(r.___errno_location=function(){return(r.___errno_location=r.asm.__errno_location).apply(null,arguments)},r._omalloc=function(){return(r._omalloc=r.asm.omalloc).apply(null,arguments)},r._ofree=function(){return(r._ofree=r.asm.ofree).apply(null,arguments)},r._getLastOnigError=function(){return(r._getLastOnigError=r.asm.getLastOnigError).apply(null,arguments)},r._createOnigScanner=function(){return(r._createOnigScanner=r.asm.createOnigScanner).apply(null,arguments)},r._freeOnigScanner=function(){return(r._freeOnigScanner=r.asm.freeOnigScanner).apply(null,arguments)},r._findNextOnigScannerMatch=function(){return(r._findNextOnigScannerMatch=r.asm.findNextOnigScannerMatch).apply(null,arguments)},r._findNextOnigScannerMatchDbg=function(){return(r._findNextOnigScannerMatchDbg=r.asm.findNextOnigScannerMatchDbg).apply(null,arguments)},r.stackSave=function(){return(r.stackSave=r.asm.stackSave).apply(null,arguments)},r.stackRestore=function(){return(r.stackRestore=r.asm.stackRestore).apply(null,arguments)},r.stackAlloc=function(){return(r.stackAlloc=r.asm.stackAlloc).apply(null,arguments)},r.dynCall_jiji=function(){return(r.dynCall_jiji=r.asm.dynCall_jiji).apply(null,arguments)},r.UTF8ToString=A,B=function O(){de||je(),de||(B=O)},r.run=je,r.preInit)for(typeof r.preInit=="function"&&(r.preInit=[r.preInit]);r.preInit.length>0;)r.preInit.pop()();return je(),y.ready});S.exports=L}},n={},function S(L){var y=n[L];if(y!==void 0)return y.exports;var E=n[L]={exports:{}};return j[L].call(E.exports,E,E.exports,S),E.exports}(770);var j,n})})(ve);var Te={exports:{}};(function(h,b){(function(j,n){h.exports=n()})(Ge,function(){return function(j){var n={};function S(L){if(n[L])return n[L].exports;var y=n[L]={i:L,l:!1,exports:{}};return j[L].call(y.exports,y,y.exports,S),y.l=!0,y.exports}return S.m=j,S.c=n,S.d=function(L,y,E){S.o(L,y)||Object.defineProperty(L,y,{enumerable:!0,get:E})},S.r=function(L){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(L,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(L,"__esModule",{value:!0})},S.t=function(L,y){if(1&y&&(L=S(L)),8&y||4&y&&typeof L=="object"&&L&&L.__esModule)return L;var E=Object.create(null);if(S.r(E),Object.defineProperty(E,"default",{enumerable:!0,value:L}),2&y&&typeof L!="string")for(var N in L)S.d(E,N,function(r){return L[r]}.bind(null,N));return E},S.n=function(L){var y=L&&L.__esModule?function(){return L.default}:function(){return L};return S.d(y,"a",y),y},S.o=function(L,y){return Object.prototype.hasOwnProperty.call(L,y)},S.p="",S(S.s=3)}([function(j,n,S){Object.defineProperty(n,"__esModule",{value:!0});var L=S(1),y=S(5),E=S(6),N=S(2),r=typeof performance>"u"?function(){return Date.now()}:function(){return performance.now()};n.createGrammar=function(f,a,m,x,C,M){return new e(f,a,m,x,C,M)};var d=function(f){this.scopeName=f};n.FullScopeDependency=d;var k=function(){function f(a,m){this.scopeName=a,this.include=m}return f.prototype.toKey=function(){return this.scopeName+"#"+this.include},f}();n.PartialScopeDependency=k;var w=function(){function f(){this.full=[],this.partial=[],this.visitedRule=new Set,this._seenFull=new Set,this._seenPartial=new Set}return f.prototype.add=function(a){a instanceof d?this._seenFull.has(a.scopeName)||(this._seenFull.add(a.scopeName),this.full.push(a)):this._seenPartial.has(a.toKey())||(this._seenPartial.add(a.toKey()),this.partial.push(a))},f}();function i(f,a,m,x,C){for(var M=0,F=x;M=0){var $=D.substring(0,z),R=D.substring(z+1);$===a.scopeName?T(f,a,a,R,B):$===m.scopeName?T(f,a,m,R,B):f.add(new k($,D.substring(z+1)))}else f.add(new d(D))}}}}function T(f,a,m,x,C){C===void 0&&(C=m.repository),C&&C[x]&&i(f,a,m,[C[x]],C)}function g(f,a,m){if(m.patterns&&Array.isArray(m.patterns)&&i(f,a,m,m.patterns,m.repository),m.injections){var x=[];for(var C in m.injections)x.push(m.injections[C]);i(f,a,m,x,m.repository)}}function l(f,a){if(!f)return!1;if(f===a)return!0;var m=a.length;return f.length>m&&f.substr(0,m)===a&&f[m]==="."}function u(f,a){if(a.length>")}var D=Object.keys(this._embeddedLanguages).map(function(z){return f._escapeRegExpCharacters(z)});D.length===0?this._embeddedLanguagesRegex=null:(D.sort(),D.reverse(),this._embeddedLanguagesRegex=new RegExp("^(("+D.join(")|(")+"))($|\\.)",""))}return f.prototype.onDidChangeTheme=function(){this._cache=new Map,this._defaultMetaData=new o("",this._initialLanguage,0,[this._themeProvider.getDefaults()])},f.prototype.getDefaultMetadata=function(){return this._defaultMetaData},f._escapeRegExpCharacters=function(a){return a.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g,"\\$&")},f.prototype.getMetadataForScope=function(a){if(a===null)return f._NULL_SCOPE_METADATA;var m=this._cache.get(a);return m||(m=this._doGetMetadataForScope(a),this._cache.set(a,m),m)},f.prototype._doGetMetadataForScope=function(a){var m=this._scopeToLanguage(a),x=this._toStandardTokenType(a),C=this._themeProvider.themeMatch(a);return new o(a,m,x,C)},f.prototype._scopeToLanguage=function(a){if(!a||!this._embeddedLanguagesRegex)return 0;var m=a.match(this._embeddedLanguagesRegex);if(!m)return 0;var x=this._embeddedLanguages[m[1]]||0;return x||0},f.prototype._toStandardTokenType=function(a){var m=a.match(f.STANDARD_TOKEN_TYPE_REGEXP);if(!m)return 0;switch(m[1]){case"comment":return 1;case"string":return 2;case"regex":return 4;case"meta.embedded":return 8}throw new Error("Unexpected match for standard token type!")},f._NULL_SCOPE_METADATA=new o("",0,0,null),f.STANDARD_TOKEN_TYPE_REGEXP=/\b(comment|string|regex|meta\.embedded)\b/,f}(),e=function(){function f(a,m,x,C,M,F){if(this._scopeMetadataProvider=new c(m,M,x),this._onigLib=F,this._rootId=-1,this._lastRuleId=0,this._ruleId2desc=[null],this._includedGrammars={},this._grammarRepository=M,this._grammar=s(a,null),this._injections=null,this._tokenTypeMatchers=[],C)for(var G=0,B=Object.keys(C);Gz)break;for(;D.length>0&&D[D.length-1].endPos<=U.start;)C.produceFromScopes(D[D.length-1].scopes,D[D.length-1].endPos),D.pop();if(D.length>0?C.produceFromScopes(D[D.length-1].scopes,U.start):C.produce(x,U.start),R.retokenizeCapturedWithRuleId){var V=R.getName(G,F),X=x.contentNameScopesList.push(f,V),Q=R.getContentName(G,F),re=X.push(f,Q),te=x.push(R.retokenizeCapturedWithRuleId,U.start,-1,!1,null,X,re),se=f.createOnigString(G.substring(0,U.end));A(f,se,m&&U.start===0,U.start,te,C,!1),t(se)}else{var Y=R.getName(G,F);if(Y!==null){var ie=(D.length>0?D[D.length-1].scopes:x.contentNameScopesList).push(f,Y);D.push(new K(ie,U.end))}}}}}for(;D.length>0;)C.produceFromScopes(D[D.length-1].scopes,D[D.length-1].endPos),D.pop()}}function v(f){for(var a=[],m=0,x=f.rules.length;m5&&console.warn("Rule "+re.debugName+" ("+re.id+") matching took "+ie+" against '"+R+"'"),Y&&console.log("matched rule id: "+te.rules[Y.index]+" from "+Y.captureIndices[0].start+" to "+Y.captureIndices[0].end)}return Y?{captureIndices:Y.captureIndices,matchedRuleId:te.rules[Y.index]}:null}(f,a,m,x,C,M),G=f.getInjections();if(G.length===0)return F;var B=function($,R,U,V,X,Q,re){for(var te,se=Number.MAX_VALUE,Y=null,ie=0,J=Q.contentNameScopesList.generateScopes(),ue=0,ae=$.length;ue=se)&&(se=de,Y=ge.captureIndices,te=ke.rules[ge.index],ie=pe.priority,se===X))break}}}return Y?{priorityMatch:ie===-1,captureIndices:Y,matchedRuleId:te}:null}(G,f,a,m,x,C,M);if(!B)return F;if(!F)return B;var D=F.captureIndices[0].start,z=B.captureIndices[0].start;return zX&&(X=ae.captureIndices[0].end,V=!1))}return{stack:Q,linePos:X,anchorPosition:te,isFirstLine:V}}(f,a,m,x,C,M);C=z.stack,x=z.linePos,m=z.isFirstLine,D=z.anchorPosition}for(;!B;)$();function $(){N.DebugFlags.InDebugMode&&(console.log(""),console.log("@@scanNext "+x+": |"+a.content.substr(x).replace(/\n$/,"\\n")+"|"));var R=P(f,a,m,x,C,D);if(!R)return N.DebugFlags.InDebugMode&&console.log(" no more matches."),M.produce(C,G),void(B=!0);var U=R.captureIndices,V=R.matchedRuleId,X=!!(U&&U.length>0)&&U[0].end>x;if(V===-1){var Q=C.getRule(f);N.DebugFlags.InDebugMode&&console.log(" popping "+Q.debugName+" - "+Q.debugEndRegExp),M.produce(C,U[0].start),C=C.setContentNameScopesList(C.nameScopesList),_(f,a,m,C,M,Q.endCaptures,U),M.produce(C,U[0].end);var re=C;if(C=C.pop(),D=re.getAnchorPos(),!X&&re.getEnterPos()===x)return N.DebugFlags.InDebugMode&&console.error("[1] - Grammar is in an endless loop - Grammar pushed & popped a rule without advancing"),C=re,M.produce(C,G),void(B=!0)}else{var te=f.getRule(V);M.produce(C,U[0].start);var se=C,Y=te.getName(a.content,U),ie=C.contentNameScopesList.push(f,Y);if(C=C.push(V,x,D,U[0].end===G,null,ie,ie),te instanceof y.BeginEndRule){var J=te;N.DebugFlags.InDebugMode&&console.log(" pushing "+J.debugName+" - "+J.debugBeginRegExp),_(f,a,m,C,M,J.beginCaptures,U),M.produce(C,U[0].end),D=U[0].end;var ue=J.getContentName(a.content,U),ae=ie.push(f,ue);if(C=C.setContentNameScopesList(ae),J.endHasBackReferences&&(C=C.setEndRule(J.getEndWithResolvedBackReferences(a.content,U))),!X&&se.hasSameRuleAs(C))return N.DebugFlags.InDebugMode&&console.error("[2] - Grammar is in an endless loop - Grammar pushed the same rule without advancing"),C=C.pop(),M.produce(C,G),void(B=!0)}else if(te instanceof y.BeginWhileRule){if(J=te,N.DebugFlags.InDebugMode&&console.log(" pushing "+J.debugName),_(f,a,m,C,M,J.beginCaptures,U),M.produce(C,U[0].end),D=U[0].end,ue=J.getContentName(a.content,U),ae=ie.push(f,ue),C=C.setContentNameScopesList(ae),J.whileHasBackReferences&&(C=C.setEndRule(J.getWhileWithResolvedBackReferences(a.content,U))),!X&&se.hasSameRuleAs(C))return N.DebugFlags.InDebugMode&&console.error("[3] - Grammar is in an endless loop - Grammar pushed the same rule without advancing"),C=C.pop(),M.produce(C,G),void(B=!0)}else{var pe=te;if(N.DebugFlags.InDebugMode&&console.log(" matched "+pe.debugName+" - "+pe.debugMatchRegExp),_(f,a,m,C,M,pe.captures,U),M.produce(C,U[0].end),C=C.pop(),!X)return N.DebugFlags.InDebugMode&&console.error("[4] - Grammar is in an endless loop - Grammar is not advancing, nor is it pushing/popping"),C=C.safePop(),M.produce(C,G),void(B=!0)}}U[0].end>x&&(x=U[0].end,m=!1)}return C}n.Grammar=e;var I=function(){function f(){}return f.toBinaryStr=function(a){for(var m=a.toString(2);m.length<32;)m="0"+m;return m},f.printMetadata=function(a){var m=f.getLanguageId(a),x=f.getTokenType(a),C=f.getFontStyle(a),M=f.getForeground(a),F=f.getBackground(a);console.log({languageId:m,tokenType:x,fontStyle:C,foreground:M,background:F})},f.getLanguageId=function(a){return(255&a)>>>0},f.getTokenType=function(a){return(1792&a)>>>8},f.getFontStyle=function(a){return(14336&a)>>>11},f.getForeground=function(a){return(8372224&a)>>>14},f.getBackground=function(a){return(4286578688&a)>>>23},f.set=function(a,m,x,C,M,F){var G=f.getLanguageId(a),B=f.getTokenType(a),D=f.getFontStyle(a),z=f.getForeground(a),$=f.getBackground(a);return m!==0&&(G=m),x!==0&&(B=x===8?0:x),C!==-1&&(D=C),M!==0&&(z=M),F!==0&&($=F),(G<<0|B<<8|D<<11|z<<14|$<<23)>>>0},f}();n.StackElementMetadata=I;var q=function(){function f(a,m,x){this.parent=a,this.scope=m,this.metadata=x}return f._equals=function(a,m){for(;;){if(a===m||!a&&!m)return!0;if(!a||!m||a.scope!==m.scope||a.metadata!==m.metadata)return!1;a=a.parent,m=m.parent}},f.prototype.equals=function(a){return f._equals(this,a)},f._matchesScope=function(a,m,x){return m===a||a.substring(0,x.length)===x},f._matches=function(a,m){if(m===null)return!0;for(var x=m.length,C=0,M=m[C],F=M+".";a;){if(this._matchesScope(a.scope,M,F)){if(++C===x)return!0;F=(M=m[C])+"."}a=a.parent}return!1},f.mergeMetadata=function(a,m,x){if(x===null)return a;var C=-1,M=0,F=0;if(x.themeData!==null)for(var G=0,B=x.themeData.length;G=0?f._push(this,a,m.split(/ /g)):f._push(this,a,[m])},f._generateScopes=function(a){for(var m=[],x=0;a;)m[x++]=a.scope,a=a.parent;return m.reverse(),m},f.prototype.generateScopes=function(){return f._generateScopes(this)},f}();n.ScopeListElement=q;var H=function(){function f(a,m,x,C,M,F,G,B){this.parent=a,this.depth=this.parent?this.parent.depth+1:1,this.ruleId=m,this._enterPos=x,this._anchorPos=C,this.beginRuleCapturedEOL=M,this.endRule=F,this.nameScopesList=G,this.contentNameScopesList=B}return f._structuralEquals=function(a,m){for(;;){if(a===m||!a&&!m)return!0;if(!a||!m||a.depth!==m.depth||a.ruleId!==m.ruleId||a.endRule!==m.endRule)return!1;a=a.parent,m=m.parent}},f._equals=function(a,m){return a===m||!!this._structuralEquals(a,m)&&a.contentNameScopesList.equals(m.contentNameScopesList)},f.prototype.clone=function(){return this},f.prototype.equals=function(a){return a!==null&&f._equals(this,a)},f._reset=function(a){for(;a;)a._enterPos=-1,a._anchorPos=-1,a=a.parent},f.prototype.reset=function(){f._reset(this)},f.prototype.pop=function(){return this.parent},f.prototype.safePop=function(){return this.parent?this.parent:this},f.prototype.push=function(a,m,x,C,M,F,G){return new f(this,a,m,x,C,M,F,G)},f.prototype.getEnterPos=function(){return this._enterPos},f.prototype.getAnchorPos=function(){return this._anchorPos},f.prototype.getRule=function(a){return a.getRule(this.ruleId)},f.prototype._writeString=function(a,m){return this.parent&&(m=this.parent._writeString(a,m)),a[m++]="("+this.ruleId+", TODO-"+this.nameScopesList+", TODO-"+this.contentNameScopesList+")",m},f.prototype.toString=function(){var a=[];return this._writeString(a,0),"["+a.join(",")+"]"},f.prototype.setContentNameScopesList=function(a){return this.contentNameScopesList===a?this:this.parent.push(this.ruleId,this._enterPos,this._anchorPos,this.beginRuleCapturedEOL,this.endRule,this.nameScopesList,a)},f.prototype.setEndRule=function(a){return this.endRule===a?this:new f(this.parent,this.ruleId,this._enterPos,this._anchorPos,this.beginRuleCapturedEOL,a,this.nameScopesList,this.contentNameScopesList)},f.prototype.hasSameRuleAs=function(a){return this.ruleId===a.ruleId},f.NULL=new f(null,0,0,0,!1,null,null,null),f}();n.StackElement=H;var K=function(f,a){this.scopes=f,this.endPos=a};n.LocalStackElement=K;var Z=function(){function f(a,m,x){this._emitBinaryTokens=a,this._tokenTypeOverrides=x,N.DebugFlags.InDebugMode?this._lineText=m:this._lineText=null,this._tokens=[],this._binaryTokens=[],this._lastTokenEndIndex=0}return f.prototype.produce=function(a,m){this.produceFromScopes(a.contentNameScopesList,m)},f.prototype.produceFromScopes=function(a,m){if(!(this._lastTokenEndIndex>=m)){if(this._emitBinaryTokens){for(var x=a.metadata,C=0,M=this._tokenTypeOverrides;C0&&this._binaryTokens[this._binaryTokens.length-1]===x||(this._binaryTokens.push(this._lastTokenEndIndex),this._binaryTokens.push(x)),void(this._lastTokenEndIndex=m)}var G=a.generateScopes();if(N.DebugFlags.InDebugMode){console.log(" token: |"+this._lineText.substring(this._lastTokenEndIndex,m).replace(/\n$/,"\\n")+"|");for(var B=0;B0&&this._tokens[this._tokens.length-1].startIndex===m-1&&this._tokens.pop(),this._tokens.length===0&&(this._lastTokenEndIndex=-1,this.produce(a,m),this._tokens[this._tokens.length-1].startIndex=0),this._tokens},f.prototype.getBinaryResult=function(a,m){this._binaryTokens.length>0&&this._binaryTokens[this._binaryTokens.length-2]===m-1&&(this._binaryTokens.pop(),this._binaryTokens.pop()),this._binaryTokens.length===0&&(this._lastTokenEndIndex=-1,this.produce(a,m),this._binaryTokens[this._binaryTokens.length-2]=0);for(var x=new Uint32Array(this._binaryTokens.length),C=0,M=this._binaryTokens.length;C0&&l[l.length-1])||t[0]!==6&&t[0]!==2)){p=0;continue}if(t[0]===3&&(!l||t[1]>l[0]&&t[1]")},w.prototype._loadGrammar=function(i,T,g,l){return L(this,void 0,void 0,function(){var u,p,o,c,e,t,s,_,v,P,A,I,q=this;return y(this,function(H){switch(H.label){case 0:u=new Set,p=new Set,u.add(i),o=[new d.FullScopeDependency(i)],H.label=1;case 1:return o.length>0?(c=o,o=[],[4,Promise.all(c.map(function(K){return q._loadSingleGrammar(K.scopeName)}))]):[3,3];case 2:for(H.sent(),e=new d.ScopeDependencyCollector,t=0,s=c;t0&&i[i.length-1])||o[0]!==6&&o[0]!==2)){g=0;continue}if(o[0]===3&&(!i||o[1]>i[0]&&o[1]v&&(v=I);for(var A=0;A<=v;A++)_[A]=null;for(var P in e)if(P!=="$vscodeTextmateLocation"){var I=parseInt(P,10),q=0;e[P].patterns&&(q=c.getCompiledRuleId(e[P],t,s)),_[I]=c.createCaptureRule(t,e[P].$vscodeTextmateLocation,e[P].name,e[P].contentName,q)}}return _},c._compilePatterns=function(e,t,s){var _=[];if(e)for(var v=0,P=e.length;v=0?(H=A.include.substring(0,Z),K=A.include.substring(Z+1)):H=A.include;var he=t.getExternalGrammar(H,s);if(he)if(K){var f=he.repository[K];f&&(I=c.getCompiledRuleId(f,t,he.repository))}else I=c.getCompiledRuleId(he.repository.$self,t,he.repository)}else I=c.getCompiledRuleId(A,t,s);if(I!==-1){var a=t.getRule(I),m=!1;if((a instanceof l||a instanceof u||a instanceof p)&&a.hasMissingPatterns&&a.patterns.length===0&&(m=!0),m)continue;_.push(I)}}return{patterns:_,hasMissingPatterns:(e?e.length:0)!==_.length}},c}();n.RuleFactory=o},function(j,n,S){function L(y){return!!y&&!!y.match(/[\w\.:]+/)}Object.defineProperty(n,"__esModule",{value:!0}),n.createMatchers=function(y,E){for(var N,r,d,k=[],w=(d=(r=/([LR]:|[\w\.:][\w\.:\-]*|[\,\|\-\(\)])/g).exec(N=y),{next:function(){if(!d)return null;var p=d[0];return d=r.exec(N),p}}),i=w.next();i!==null;){var T=0;if(i.length===2&&i.charAt(1)===":"){switch(i.charAt(0)){case"R":T=1;break;case"L":T=-1;break;default:console.log("Unknown priority "+i+" in scope selector")}i=w.next()}var g=u();if(k.push({matcher:g,priority:T}),i!==",")break;i=w.next()}return k;function l(){if(i==="-"){i=w.next();var p=l();return function(e){return!!p&&!p(e)}}if(i==="("){i=w.next();var o=function(){for(var e=[],t=u();t&&(e.push(t),i==="|"||i===",");){do i=w.next();while(i==="|"||i===",");t=u()}return function(s){return e.some(function(_){return _(s)})}}();return i===")"&&(i=w.next()),o}if(L(i)){var c=[];do c.push(i),i=w.next();while(L(i));return function(e){return E(c,e)}}return null}function u(){for(var p=[],o=l();o;)p.push(o),o=l();return function(c){return p.every(function(e){return e(c)})}}}},function(j,n){var S,L,y=j.exports={};function E(){throw new Error("setTimeout has not been defined")}function N(){throw new Error("clearTimeout has not been defined")}function r(p){if(S===setTimeout)return setTimeout(p,0);if((S===E||!S)&&setTimeout)return S=setTimeout,setTimeout(p,0);try{return S(p,0)}catch{try{return S.call(null,p,0)}catch{return S.call(this,p,0)}}}(function(){try{S=typeof setTimeout=="function"?setTimeout:E}catch{S=E}try{L=typeof clearTimeout=="function"?clearTimeout:N}catch{L=N}})();var d,k=[],w=!1,i=-1;function T(){w&&d&&(w=!1,d.length?k=d.concat(k):i=-1,k.length&&g())}function g(){if(!w){var p=r(T);w=!0;for(var o=k.length;o;){for(d=k,k=[];++i1)for(var c=1;c0;)y.charCodeAt(d)===10?(d++,k++,w=0):(d++,w++),R--}function T(R){N===null?d=R:i(R-d)}function g(){for(;d0&&y.charCodeAt(0)===65279&&(d=1);var o=0,c=null,e=[],t=[],s=null;function _(R,U){e.push(o),t.push(c),o=R,c=U}function v(){if(e.length===0)return P("illegal state stack");o=e.pop(),c=t.pop()}function P(R){throw new Error("Near offset "+d+": "+R+" ~~~"+y.substr(d,50)+"~~~")}var A,I,q,H=function(){if(s===null)return P("missing ");var R={};N!==null&&(R[N]={filename:E,line:k,char:w}),c[s]=R,s=null,_(1,R)},K=function(){if(s===null)return P("missing ");var R=[];c[s]=R,s=null,_(2,R)},Z=function(){var R={};N!==null&&(R[N]={filename:E,line:k,char:w}),c.push(R),_(1,R)},he=function(){var R=[];c.push(R),_(2,R)};function f(){if(o!==1)return P("unexpected ");v()}function a(){return o===1||o!==2?P("unexpected "):void v()}function m(R){if(o===1){if(s===null)return P("missing ");c[s]=R,s=null}else o===2?c.push(R):c=R}function x(R){if(isNaN(R))return P("cannot parse float");if(o===1){if(s===null)return P("missing ");c[s]=R,s=null}else o===2?c.push(R):c=R}function C(R){if(isNaN(R))return P("cannot parse integer");if(o===1){if(s===null)return P("missing ");c[s]=R,s=null}else o===2?c.push(R):c=R}function M(R){if(o===1){if(s===null)return P("missing ");c[s]=R,s=null}else o===2?c.push(R):c=R}function F(R){if(o===1){if(s===null)return P("missing ");c[s]=R,s=null}else o===2?c.push(R):c=R}function G(R){if(o===1){if(s===null)return P("missing ");c[s]=R,s=null}else o===2?c.push(R):c=R}function B(R){if(R.isClosed)return"";var U=p(""),U.replace(/&#([0-9]+);/g,function(V,X){return String.fromCodePoint(parseInt(X,10))}).replace(/&#x([0-9a-f]+);/g,function(V,X){return String.fromCodePoint(parseInt(X,16))}).replace(/&|<|>|"|'/g,function(V){switch(V){case"&":return"&";case"<":return"<";case">":return">";case""":return'"';case"'":return"'"}return V})}for(;d=r));){var D=y.charCodeAt(d);if(i(1),D!==60)return P("expected <");if(d>=r)return P("unexpected end of input");var z=y.charCodeAt(d);if(z!==63)if(z!==33){if(z===47){if(i(1),g(),l("plist")){u(">");continue}if(l("dict")){u(">"),f();continue}if(l("array")){u(">"),a();continue}return P("unexpected closed tag")}var $=(I=void 0,q=void 0,I=p(">"),q=!1,I.charCodeAt(I.length-1)===47&&(q=!0,I=I.substring(0,I.length-1)),{name:I.trim(),isClosed:q});switch($.name){case"dict":o===1?H():o===2?Z():(c={},N!==null&&(c[N]={filename:E,line:k,char:w}),_(1,c)),$.isClosed&&f();continue;case"array":o===1?K():o===2?he():_(2,c=[]),$.isClosed&&a();continue;case"key":A=B($),o!==1?P("unexpected "):s!==null?P("too many "):s=A;continue;case"string":m(B($));continue;case"real":x(parseFloat(B($)));continue;case"integer":C(parseInt(B($),10));continue;case"date":M(new Date(B($)));continue;case"data":F(B($));continue;case"true":B($),G(!0);continue;case"false":B($),G(!1);continue}if(!/^plist/.test($.name))return P("unexpected opened tag "+$.name)}else{if(i(1),l("--")){u("-->");continue}u(">")}else i(1),u("?>")}return c}Object.defineProperty(n,"__esModule",{value:!0}),n.parseWithLocation=function(y,E,N){return L(y,E,N)},n.parse=function(y){return L(y,null,null)}},function(j,n,S){function L(r,d){throw new Error("Near offset "+r.pos+": "+d+" ~~~"+r.source.substr(r.pos,50)+"~~~")}Object.defineProperty(n,"__esModule",{value:!0}),n.parse=function(r,d,k){var w=new y(r),i=new E,T=0,g=null,l=[],u=[];function p(){l.push(T),u.push(g)}function o(){T=l.pop(),g=u.pop()}function c(_){L(w,_)}for(;N(w,i);){if(T===0){if(g!==null&&c("too many constructs in root"),i.type===3){g={},k&&(g.$vscodeTextmateLocation=i.toLocation(d)),p(),T=1;continue}if(i.type===2){g=[],p(),T=4;continue}c("unexpected token in root")}if(T===2){if(i.type===5){o();continue}if(i.type===7){T=3;continue}c("expected , or }")}if(T===1||T===3){if(T===1&&i.type===5){o();continue}if(i.type===1){var e=i.value;if(N(w,i)&&i.type===6||c("expected colon"),N(w,i)||c("expected value"),T=2,i.type===1){g[e]=i.value;continue}if(i.type===8){g[e]=null;continue}if(i.type===9){g[e]=!0;continue}if(i.type===10){g[e]=!1;continue}if(i.type===11){g[e]=parseFloat(i.value);continue}if(i.type===2){var t=[];g[e]=t,p(),T=4,g=t;continue}if(i.type===3){var s={};k&&(s.$vscodeTextmateLocation=i.toLocation(d)),g[e]=s,p(),T=1,g=s;continue}}c("unexpected token in dict")}if(T===5){if(i.type===4){o();continue}if(i.type===7){T=6;continue}c("expected , or ]")}if(T===4||T===6){if(T===4&&i.type===4){o();continue}if(T=5,i.type===1){g.push(i.value);continue}if(i.type===8){g.push(null);continue}if(i.type===9){g.push(!0);continue}if(i.type===10){g.push(!1);continue}if(i.type===11){g.push(parseFloat(i.value));continue}if(i.type===2){t=[],g.push(t),p(),T=4,g=t;continue}if(i.type===3){s={},k&&(s.$vscodeTextmateLocation=i.toLocation(d)),g.push(s),p(),T=1,g=s;continue}c("unexpected token in array")}c("unknown state")}return u.length!==0&&c("unclosed constructs"),g};var y=function(r){this.source=r,this.pos=0,this.len=r.length,this.line=1,this.char=0},E=function(){function r(){this.value=null,this.type=0,this.offset=-1,this.len=-1,this.line=-1,this.char=-1}return r.prototype.toLocation=function(d){return{filename:d,line:this.line,char:this.char}},r}();function N(r,d){d.value=null,d.type=0,d.offset=-1,d.len=-1,d.line=-1,d.char=-1;for(var k,w=r.source,i=r.pos,T=r.len,g=r.line,l=r.char;;){if(i>=T)return!1;if((k=w.charCodeAt(i))!==32&&k!==9&&k!==13){if(k!==10)break;i++,g++,l=0}else i++,l++}if(d.offset=i,d.line=g,d.char=l,k===34){for(d.type=1,i++,l++;;){if(i>=T)return!1;if(k=w.charCodeAt(i),i++,l++,k!==92){if(k===34)break}else i++,l++}d.value=w.substring(d.offset+1,i-1).replace(/\\u([0-9A-Fa-f]{4})/g,function(u,p){return String.fromCodePoint(parseInt(p,16))}).replace(/\\(.)/g,function(u,p){switch(p){case'"':return'"';case"\\":return"\\";case"/":return"/";case"b":return"\b";case"f":return"\f";case"n":return` +`;case"r":return"\r";case"t":return" ";default:L(r,"invalid escape sequence")}throw new Error("unreachable")})}else if(k===91)d.type=2,i++,l++;else if(k===123)d.type=3,i++,l++;else if(k===93)d.type=4,i++,l++;else if(k===125)d.type=5,i++,l++;else if(k===58)d.type=6,i++,l++;else if(k===44)d.type=7,i++,l++;else if(k===110){if(d.type=8,i++,l++,(k=w.charCodeAt(i))!==117||(i++,l++,(k=w.charCodeAt(i))!==108)||(i++,l++,(k=w.charCodeAt(i))!==108))return!1;i++,l++}else if(k===116){if(d.type=9,i++,l++,(k=w.charCodeAt(i))!==114||(i++,l++,(k=w.charCodeAt(i))!==117)||(i++,l++,(k=w.charCodeAt(i))!==101))return!1;i++,l++}else if(k===102){if(d.type=10,i++,l++,(k=w.charCodeAt(i))!==97||(i++,l++,(k=w.charCodeAt(i))!==108)||(i++,l++,(k=w.charCodeAt(i))!==115)||(i++,l++,(k=w.charCodeAt(i))!==101))return!1;i++,l++}else for(d.type=11;;){if(i>=T)return!1;if(!((k=w.charCodeAt(i))===46||k>=48&&k<=57||k===101||k===69||k===45||k===43))break;i++,l++}return d.len=i-d.offset,d.value===null&&(d.value=w.substr(d.offset,d.len)),r.pos=i,r.line=g,r.char=l,!0}},function(j,n,S){Object.defineProperty(n,"__esModule",{value:!0});var L=function(g,l,u,p,o,c){this.scope=g,this.parentScopes=l,this.index=u,this.fontStyle=p,this.foreground=o,this.background=c};function y(g){return!!/^#[0-9a-f]{6}$/i.test(g)||!!/^#[0-9a-f]{8}$/i.test(g)||!!/^#[0-9a-f]{3}$/i.test(g)||!!/^#[0-9a-f]{4}$/i.test(g)}function E(g){if(!g)return[];if(!g.settings||!Array.isArray(g.settings))return[];for(var l=g.settings,u=[],p=0,o=0,c=l.length;o1&&(H=I.slice(0,I.length-1)).reverse(),u[p++]=new L(q,H,o,s,P,A)}}}return u}function N(g,l){g.sort(function(A,I){var q=k(A.scope,I.scope);return q!==0||(q=w(A.parentScopes,I.parentScopes))!==0?q:A.index-I.index});for(var u=0,p="#000000",o="#ffffff";g.length>=1&&g[0].scope==="";){var c=g.shift();c.fontStyle!==-1&&(u=c.fontStyle),c.foreground!==null&&(p=c.foreground),c.background!==null&&(o=c.background)}for(var e=new r(l),t=new i(0,null,u,e.getId(p),e.getId(o)),s=new T(new i(0,null,-1,0,0),[]),_=0,v=g.length;_l?1:0}function w(g,l){if(g===null&&l===null)return 0;if(!g)return-1;if(!l)return 1;var u=g.length,p=l.length;if(u===p){for(var o=0;ol?console.log("how did this happen?"):this.scopeDepth=l,u!==-1&&(this.fontStyle=u),p!==0&&(this.foreground=p),o!==0&&(this.background=o)},g}();n.ThemeTrieElementRule=i;var T=function(){function g(l,u,p){u===void 0&&(u=[]),p===void 0&&(p={}),this._mainRule=l,this._rulesWithParentScopes=u,this._children=p}return g._sortBySpecificity=function(l){return l.length===1||l.sort(this._cmpBySpecificity),l},g._cmpBySpecificity=function(l,u){if(l.scopeDepth===u.scopeDepth){var p=l.parentScopes,o=u.parentScopes,c=p===null?0:p.length,e=o===null?0:o.length;if(c===e)for(var t=0;t>>0}static getTokenType(b){return(b&1792)>>>8}static getFontStyle(b){return(b&14336)>>>11}static getForeground(b){return(b&8372224)>>>14}static getBackground(b){return(b&4286578688)>>>23}static set(b,j,n,S,L,y){let E=le.getLanguageId(b),N=le.getTokenType(b),r=le.getFontStyle(b),d=le.getForeground(b),k=le.getBackground(b);return j!==0&&(E=j),n!==0&&(N=n===8?0:n),S!==be.NotSet&&(r=S),L!==0&&(d=L),y!==0&&(k=y),(E<<0|N<<8|r<<11|d<<14|k<<23)>>>0}}function qe(h){return h.endsWith("/")||h.endsWith("\\")?h.slice(0,-1):h}function ze(h){return h.startsWith("./")?h.slice(2):h}function He(h){const b=h.split(/[\/\\]/g);return b[b.length-2]}function Ve(...h){return h.map(qe).map(ze).join("/")}function Ke(h,b){const j=new Map;for(const n of h){const S=b(n);j.has(S)?j.get(S).push(n):j.set(S,[n])}return j}function Xe(h,b){b===void 0&&(b=!1);var j=h.length,n=0,S="",L=0,y=16,E=0,N=0,r=0,d=0,k=0;function w(o,c){for(var e=0,t=0;e=48&&s<=57)t=t*16+s-48;else if(s>=65&&s<=70)t=t*16+s-65+10;else if(s>=97&&s<=102)t=t*16+s-97+10;else break;n++,e++}return e=j){o+=h.substring(c,n),k=2;break}var e=h.charCodeAt(n);if(e===34){o+=h.substring(c,n),n++;break}if(e===92){if(o+=h.substring(c,n),n++,n>=j){k=2;break}var t=h.charCodeAt(n++);switch(t){case 34:o+='"';break;case 92:o+="\\";break;case 47:o+="/";break;case 98:o+="\b";break;case 102:o+="\f";break;case 110:o+=` +`;break;case 114:o+="\r";break;case 116:o+=" ";break;case 117:var s=w(4,!0);s>=0?o+=String.fromCharCode(s):k=4;break;default:k=5}c=n;continue}if(e>=0&&e<=31)if(Le(e)){o+=h.substring(c,n),k=2;break}else k=6;n++}return o}function l(){if(S="",k=0,L=n,N=E,d=r,n>=j)return L=j,y=17;var o=h.charCodeAt(n);if(Ce(o)){do n++,S+=String.fromCharCode(o),o=h.charCodeAt(n);while(Ce(o));return y=15}if(Le(o))return n++,S+=String.fromCharCode(o),o===13&&h.charCodeAt(n)===10&&(n++,S+=` +`),E++,r=n,y=14;switch(o){case 123:return n++,y=1;case 125:return n++,y=2;case 91:return n++,y=3;case 93:return n++,y=4;case 58:return n++,y=6;case 44:return n++,y=5;case 34:return n++,S=g(),y=10;case 47:var c=n-1;if(h.charCodeAt(n+1)===47){for(n+=2;n=12&&o<=15);return o}return{setPosition:i,getPosition:function(){return n},scan:b?p:l,getToken:function(){return y},getTokenValue:function(){return S},getTokenOffset:function(){return L},getTokenLength:function(){return n-L},getTokenStartLine:function(){return N},getTokenStartCharacter:function(){return L-d},getTokenError:function(){return k}}}function Ce(h){return h===32||h===9||h===11||h===12||h===160||h===5760||h>=8192&&h<=8203||h===8239||h===8287||h===12288||h===65279}function Le(h){return h===10||h===13||h===8232||h===8233}function ye(h){return h>=48&&h<=57}var we;(function(h){h.DEFAULT={allowTrailingComma:!1}})(we||(we={}));function Je(h,b,j){b===void 0&&(b=[]),j===void 0&&(j=we.DEFAULT);var n=null,S=[],L=[];function y(N){Array.isArray(S)?S.push(N):n!==null&&(S[n]=N)}var E={onObjectBegin:function(){var N={};y(N),L.push(S),S=N,n=null},onObjectProperty:function(N){n=N},onObjectEnd:function(){S=L.pop()},onArrayBegin:function(){var N=[];y(N),L.push(S),S=N,n=null},onArrayEnd:function(){S=L.pop()},onLiteralValue:y,onError:function(N,r,d){b.push({error:N,offset:r,length:d})}};return Ye(h,E,j),S[0]}function Ye(h,b,j){j===void 0&&(j=we.DEFAULT);var n=Xe(h,!1);function S(v){return v?function(){return v(n.getTokenOffset(),n.getTokenLength(),n.getTokenStartLine(),n.getTokenStartCharacter())}:function(){return!0}}function L(v){return v?function(P){return v(P,n.getTokenOffset(),n.getTokenLength(),n.getTokenStartLine(),n.getTokenStartCharacter())}:function(){return!0}}var y=S(b.onObjectBegin),E=L(b.onObjectProperty),N=S(b.onObjectEnd),r=S(b.onArrayBegin),d=S(b.onArrayEnd),k=L(b.onLiteralValue),w=L(b.onSeparator),i=S(b.onComment),T=L(b.onError),g=j&&j.disallowComments,l=j&&j.allowTrailingComma;function u(){for(;;){var v=n.scan();switch(n.getTokenError()){case 4:p(14);break;case 5:p(15);break;case 3:p(13);break;case 1:g||p(11);break;case 2:p(12);break;case 6:p(16);break}switch(v){case 12:case 13:g?p(10):i();break;case 16:p(1);break;case 15:case 14:break;default:return v}}}function p(v,P,A){if(P===void 0&&(P=[]),A===void 0&&(A=[]),T(v),P.length+A.length>0)for(var I=n.getToken();I!==17;){if(P.indexOf(I)!==-1){u();break}else if(A.indexOf(I)!==-1)break;I=u()}}function o(v){var P=n.getTokenValue();return v?k(P):E(P),u(),!0}function c(){switch(n.getToken()){case 11:var v=n.getTokenValue(),P=Number(v);isNaN(P)&&(p(2),P=0),k(P);break;case 7:k(null);break;case 8:k(!0);break;case 9:k(!1);break;default:return!1}return u(),!0}function e(){return n.getToken()!==10?(p(3,[],[2,5]),!1):(o(!1),n.getToken()===6?(w(":"),u(),_()||p(4,[],[2,5])):p(5,[],[2,5]),!0)}function t(){y(),u();for(var v=!1;n.getToken()!==2&&n.getToken()!==17;){if(n.getToken()===5){if(v||p(4,[],[]),w(","),u(),n.getToken()===2&&l)break}else v&&p(6,[],[]);e()||p(4,[],[2,5]),v=!0}return N(),n.getToken()!==2?p(7,[2],[]):u(),!0}function s(){r(),u();for(var v=!1;n.getToken()!==4&&n.getToken()!==17;){if(n.getToken()===5){if(v||p(4,[],[]),w(","),u(),n.getToken()===4&&l)break}else v&&p(6,[],[]);_()||p(4,[],[4,5]),v=!0}return d(),n.getToken()!==4?p(8,[4],[]):u(),!0}function _(){switch(n.getToken()){case 3:return s();case 1:return t();case 10:return o(!0);default:return c()}}return u(),n.getToken()===17?j.allowEmptyContent?!0:(p(4,[],[]),!1):_()?(n.getToken()!==17&&p(9,[],[]),!0):(p(4,[],[]),!1)}var Qe=Je;const Ze=typeof self<"u"&&typeof self.WorkerGlobalScope<"u",Ae=Ze||typeof window<"u"&&typeof window.document<"u"&&typeof fetch<"u";let Ne="",Pe="";function vt(h){Ne=h}function et(h){Pe=h}let Se=null;async function tt(){if(!Se){let h;if(Ae)typeof Pe=="string"?h=ve.exports.loadWASM({data:await fetch(Be("dist/onig.wasm")).then(b=>b.arrayBuffer())}):h=ve.exports.loadWASM(Pe);else{const j=require("path").join(require.resolve("vscode-oniguruma"),"../onig.wasm"),S=require("fs").readFileSync(j).buffer;h=ve.exports.loadWASM(S)}Se=h.then(()=>({createOnigScanner(b){return ve.exports.createOnigScanner(b)},createOnigString(b){return ve.exports.createOnigString(b)}}))}return Se}function Be(h){if(Ae)return Ne||console.warn("[Shiki] no CDN provider found, use `setCDN()` to specify the CDN for loading the resources before calling `getHighlighter()`"),`${Ne}${h}`;{const b=require("path");return b.isAbsolute(h)?h:b.resolve(__dirname,"..",h)}}async function nt(h){const b=Be(h);return Ae?await fetch(b).then(j=>j.text()):await require("fs").promises.readFile(b,"utf-8")}async function We(h){const b=[],j=Qe(await nt(h),b,{allowTrailingComma:!0});if(b.length)throw b[0];return j}async function Fe(h){let b=await We(h);const j=Ue(b);if(j.include){const n=await Fe(Ve(He(h),j.include));n.settings&&(j.settings=n.settings.concat(j.settings)),n.bg&&!j.bg&&(j.bg=n.bg),n.colors&&(j.colors=Object.assign(Object.assign({},n.colors),j.colors)),delete j.include}return j}async function rt(h){return await We(h)}function st(h){h.settings||(h.settings=[]),!(h.settings[0]&&h.settings[0].settings&&!h.settings[0].scope)&&h.settings.unshift({settings:{foreground:h.fg,background:h.bg}})}function Ue(h){const b=h.type||"dark",j=Object.assign(Object.assign({name:h.name,type:b},h),at(h));return h.include&&(j.include=h.include),h.tokenColors&&(j.settings=h.tokenColors,delete j.tokenColors),st(j),j}const Ee={light:"#333333",dark:"#bbbbbb"},Me={light:"#fffffe",dark:"#1e1e1e"};function at(h){var b,j,n,S,L,y;let E,N,r=h.settings?h.settings:h.tokenColors;const d=r?r.find(k=>!k.name&&!k.scope):void 0;return!((b=d==null?void 0:d.settings)===null||b===void 0)&&b.foreground&&(E=d.settings.foreground),!((j=d==null?void 0:d.settings)===null||j===void 0)&&j.background&&(N=d.settings.background),!E&&((S=(n=h)===null||n===void 0?void 0:n.colors)===null||S===void 0?void 0:S["editor.foreground"])&&(E=h.colors["editor.foreground"]),!N&&((y=(L=h)===null||L===void 0?void 0:L.colors)===null||y===void 0?void 0:y["editor.background"])&&(N=h.colors["editor.background"]),E||(E=h.type==="light"?Ee.light:Ee.dark),N||(N=h.type==="light"?Me.light:Me.dark),{fg:E,bg:N}}class ot{constructor(b,j){this.languagesPath="languages/",this.languageMap={},this.scopeToLangMap={},this._onigLibPromise=b,this._onigLibName=j}get onigLib(){return this._onigLibPromise}getOnigLibName(){return this._onigLibName}getLangRegistration(b){return this.languageMap[b]}async loadGrammar(b){const j=this.scopeToLangMap[b];if(!j)return null;if(j.grammar)return j.grammar;const n=await rt(xe.includes(j)?`${this.languagesPath}${j.path}`:j.path);return j.grammar=n,n}addLanguage(b){this.languageMap[b.id]=b,b.aliases&&b.aliases.forEach(j=>{this.languageMap[j]=b}),this.scopeToLangMap[b.scopeName]=b}}function it(h,b,j,n,S){let L=j.split(/\r\n|\r|\n/),y=Te.exports.INITIAL,E=[],N=[];for(let r=0,d=L.length;r=0&&L>=0;)De(b[S],n[L])&&S--,L--;return S===-1}function lt(h,b,j){let n=[],S=0;for(let L=0,y=h.settings.length;Lr.trim());else if(Array.isArray(E.scope))N=E.scope;else continue;for(let r=0,d=N.length;ry.line);let L="";return L+=`
    `,b.langId&&(L+=`
    ${b.langId}
    `),L+="",h.forEach((y,E)=>{var N;const r=E+1,d=(N=S.get(r))!==null&&N!==void 0?N:[],k=dt(d).join(" ");L+=``,y.forEach(w=>{const i=[`color: ${w.color||b.fg}`];w.fontStyle&be.Italic&&i.push("font-style: italic"),w.fontStyle&be.Bold&&i.push("font-weight: bold"),w.fontStyle&be.Underline&&i.push("text-decoration: underline"),L+=`${ft(w.content)}`}),L+=` +`}),L=L.replace(/\n*$/,""),L+="
    ",L}const ht={"&":"&","<":"<",">":">",'"':""","'":"'"};function ft(h){return h.replace(/[&<>"']/g,b=>ht[b])}function dt(h){var b;const j=new Set(["line"]);for(const n of h)for(const S of(b=n.classes)!==null&&b!==void 0?b:[])j.add(S);return Array.from(j)}class gt extends Te.exports.Registry{constructor(b){super(b),this._resolver=b,this.themesPath="themes/",this._resolvedThemes={},this._resolvedGrammars={}}getTheme(b){return typeof b=="string"?this._resolvedThemes[b]:b}async loadTheme(b){return typeof b=="string"?(this._resolvedThemes[b]||(this._resolvedThemes[b]=await Fe(`${this.themesPath}${b}.json`)),this._resolvedThemes[b]):(b=Ue(b),b.name&&(this._resolvedThemes[b.name]=b),b)}async loadThemes(b){return await Promise.all(b.map(j=>this.loadTheme(j)))}getLoadedThemes(){return Object.keys(this._resolvedThemes)}getGrammar(b){return this._resolvedGrammars[b]}async loadLanguage(b){const j=await this.loadGrammar(b.scopeName);this._resolvedGrammars[b.id]=j,b.aliases&&b.aliases.forEach(n=>{this._resolvedGrammars[n]=j})}async loadLanguages(b){for(const j of b)this._resolver.addLanguage(j);for(const j of b)await this.loadLanguage(j)}getLoadedLanguages(){return Object.keys(this._resolvedGrammars)}}function $e(h){return typeof h=="string"?xe.find(b=>{var j;return b.id===h||((j=b.aliases)===null||j===void 0?void 0:j.includes(h))}):h}function mt(h){var b;let j=xe,n=h.themes||[];return!((b=h.langs)===null||b===void 0)&&b.length&&(j=h.langs.map($e)),h.theme&&n.unshift(h.theme),n.length||(n=["nord"]),{_languages:j,_themes:n}}async function bt(h){var b,j;const{_languages:n,_themes:S}=mt(h),L=new ot(tt(),"vscode-oniguruma"),y=new gt(L);!((b=h.paths)===null||b===void 0)&&b.themes&&(y.themesPath=h.paths.themes),!((j=h.paths)===null||j===void 0)&&j.languages&&(L.languagesPath=h.paths.languages);const N=(await y.loadThemes(S))[0];let r;await y.loadLanguages(n);const d={"#000001":"var(--shiki-color-text)","#000002":"var(--shiki-color-background)","#000004":"var(--shiki-token-constant)","#000005":"var(--shiki-token-string)","#000006":"var(--shiki-token-comment)","#000007":"var(--shiki-token-keyword)","#000008":"var(--shiki-token-parameter)","#000009":"var(--shiki-token-function)","#000010":"var(--shiki-token-string-expression)","#000011":"var(--shiki-token-punctuation)","#000012":"var(--shiki-token-link)"};function k(t,s){t.bg=d[t.bg]||t.bg,t.fg=d[t.fg]||t.fg,s.forEach((_,v)=>{s[v]=d[_]||_})}function w(t){const s=t?y.getTheme(t):N;if(!s)throw Error(`No theme registration for ${t}`);(!r||r.name!==s.name)&&(y.setTheme(s),r=s);const _=y.getColorMap();return s.name==="css-variables"&&k(s,_),{_theme:s,_colorMap:_}}function i(t){const s=y.getGrammar(t);if(!s)throw Error(`No language registration for ${t}`);return{_grammar:s}}function T(t,s="text",_,v={includeExplanation:!0}){if(_t(s))return[...t.split(/\r\n|\r|\n/).map(H=>[{content:H}])];const{_grammar:P}=i(s),{_theme:A,_colorMap:I}=w(_);return it(A,I,t,P,v)}function g(t,s="text",_){let v;typeof s=="object"?v=s:v={lang:s,theme:_};const P=T(t,v.lang,v.theme,{includeExplanation:!1}),{_theme:A}=w(v.theme);return pt(P,{fg:A.fg,bg:A.bg,lineOptions:v==null?void 0:v.lineOptions})}async function l(t){await y.loadTheme(t)}async function u(t){const s=$e(t);L.addLanguage(s),await y.loadLanguage(s)}function p(){return y.getLoadedThemes()}function o(){return y.getLoadedLanguages()}function c(t){const{_theme:s}=w(t);return s.bg}function e(t){const{_theme:s}=w(t);return s.fg}return{codeToThemedTokens:T,codeToHtml:g,getTheme:t=>w(t)._theme,loadTheme:l,loadLanguage:u,getBackgroundColor:c,getForegroundColor:e,getLoadedThemes:p,getLoadedLanguages:o}}function _t(h){return!h||["plaintext","txt","text"].includes(h)}function kt(h){et(h)}export{xe as BUNDLED_LANGUAGES,yt as BUNDLED_THEMES,be as FontStyle,bt as getHighlighter,Fe as loadTheme,pt as renderToHtml,vt as setCDN,kt as setOnigasmWASM,et as setWasm,Ue as toShikiTheme}; diff --git a/assets/index.html.5d377a5b.js b/assets/index.html.5d377a5b.js new file mode 100644 index 000000000..5339e836a --- /dev/null +++ b/assets/index.html.5d377a5b.js @@ -0,0 +1 @@ +import{_ as s,y as r,z as d,X as e,B as t,Q as o,$ as i,a5 as c,P as l}from"./framework.fe9a73df.js";const u={},h=e("h1",{id:"coalesce",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#coalesce","aria-hidden":"true"},"#"),t(" Coalesce")],-1),p={href:"https://intellitect.com",target:"_blank",rel:"noopener noreferrer"},_=e("li",null,"ASP.NET Core",-1),m=e("li",null,"EF Core",-1),f=e("li",null,"TypeScript",-1),g={href:"https://vuejs.org/",target:"_blank",rel:"noopener noreferrer"},k=c('

    What do I do?

    You are responsible for the interesting parts of your application:

    • Data Model
    • Business Logic
    • External Integrations
    • Page Content
    • Site Design
    • Custom Scripting

    What is done for me?

    Coalesce builds the part of your application that are mundane and monotonous to build:

    ',5),w=e("li",null,"APIs to interact with your models via endpoints like List, Get, Save, and more.",-1),y=e("li",null,"A complete set of admin pages are provided, allowing you to read, create, edit, and delete data straight away without writing any additional code.",-1),b=e("h1",{id:"getting-started",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#getting-started","aria-hidden":"true"},"#"),t(" Getting Started")],-1);function v(x,C){const n=l("ExternalLinkIcon"),a=l("RouterLink");return r(),d("div",null,[h,e("p",null,[t("Designed to help you quickly build amazing web applications, Coalesce is a rapid-development code generation framework, created by "),e("a",p,[t("IntelliTect"),o(n)]),t(" and built on top of:")]),e("ul",null,[_,m,f,e("li",null,[e("a",g,[t("Vue"),o(n)])])]),k,e("ul",null,[e("li",null,[t("Client side "),o(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:i(()=>[t("TypeScript ViewModels")]),_:1}),t(" that mirror your data model for both lists and individual objects. Utilize these to rapidly build out your application's various pages.")]),w,e("li",null,[t("Out-of-the-box "),o(a,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:i(()=>[t("Vue Components")]),_:1}),t(" for common controls like dates, selecting objects via drop downs, enums, etc. Dropdowns support searching and paging automatically.")]),y]),b,e("p",null,[t("To get started with Coalesce, check out "),o(a,{to:"/stacks/vue/getting-started.html"},{default:i(()=>[t("Getting Started with Vue")]),_:1}),t(".")]),e("p",null,[t("While Knockout.js is still supported by Coalesce, it is a deprecated option and not recommended for new projects. If you do still want to choose Knockout, click "),o(a,{to:"/stacks/ko/getting-started.html"},{default:i(()=>[t("here")]),_:1}),t(".")])])}const V=s(u,[["render",v],["__file","index.html.vue"]]);export{V as default}; diff --git a/assets/index.html.6f4afde9.js b/assets/index.html.6f4afde9.js new file mode 100644 index 000000000..a12d31cf8 --- /dev/null +++ b/assets/index.html.6f4afde9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"Coalesce Documentation","lang":"en-US","frontmatter":{"lang":"en-US","title":"Coalesce Documentation","description":"Documentation home page for IntelliTect.Coalesce"},"excerpt":"","headers":[{"level":2,"title":"What do I do?","slug":"what-do-i-do","link":"#what-do-i-do","children":[]},{"level":2,"title":"What is done for me?","slug":"what-is-done-for-me","link":"#what-is-done-for-me","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"index.md"}');export{e as data}; diff --git a/assets/inject.html.6b988db4.js b/assets/inject.html.6b988db4.js new file mode 100644 index 000000000..311e9e92c --- /dev/null +++ b/assets/inject.html.6b988db4.js @@ -0,0 +1,13 @@ +import{_ as o,y as p,z as t,X as n,B as s,Q as l,$ as e,a5 as c,P as r}from"./framework.fe9a73df.js";const D={},i=n("h1",{id:"inject",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#inject","aria-hidden":"true"},"#"),s(" [Inject]")],-1),y=n("p",null,"========",-1),d=n("code",null,"IServiceProvider",-1),u=c(`

    This gets translated to a Microsoft.AspNetCore.Mvc.FromServicesAttribute in the generated API controller's action.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string FirstName { get; set; }
    +    public string LastName { get; set; }
    +
    +    public string GetFullName([Inject] ILogger<Person> logger)
    +    {
    +        logger.LogInformation("Person " + PersonId + "'s full name was requested");
    +        return FirstName + " " + LastName";
    +    }
    +}
    +
    `,3);function C(m,v){const a=r("RouterLink");return p(),t("div",null,[i,y,n("p",null,[s("Used to mark a "),l(a,{to:"/modeling/model-components/methods.html"},{default:e(()=>[s("Method")]),_:1}),s(" parameter for dependency injection from the application's "),d,s(".")]),n("p",null,[s("See "),l(a,{to:"/modeling/model-components/methods.html"},{default:e(()=>[s("Methods")]),_:1}),s(" for more.")]),u])}const g=o(D,[["render",C],["__file","inject.html.vue"]]);export{g as default}; diff --git a/assets/inject.html.859d9f07.js b/assets/inject.html.859d9f07.js new file mode 100644 index 000000000..e115049bf --- /dev/null +++ b/assets/inject.html.859d9f07.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-60941fa8","path":"/modeling/model-components/attributes/inject.html","title":"[Inject]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/inject.md"}');export{e as data}; diff --git a/assets/internal-use.html.bddc07d7.js b/assets/internal-use.html.bddc07d7.js new file mode 100644 index 000000000..a8882d1bc --- /dev/null +++ b/assets/internal-use.html.bddc07d7.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-440fbe82","path":"/modeling/model-components/attributes/internal-use.html","title":"[InternalUse]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/internal-use.md"}');export{e as data}; diff --git a/assets/internal-use.html.d0d63e3d.js b/assets/internal-use.html.d0d63e3d.js new file mode 100644 index 000000000..6639c3350 --- /dev/null +++ b/assets/internal-use.html.d0d63e3d.js @@ -0,0 +1,21 @@ +import{_ as o,y as p,z as t,X as s,B as n,Q as l,$ as e,a5 as D,P as r}from"./framework.fe9a73df.js";const c={},i=s("h1",{id:"internaluse",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#internaluse","aria-hidden":"true"},"#"),n(" [InternalUse]")],-1),y=s("p",null,"Used to mark a type, property or method for internal use. Internal Use members are:",-1),d=s("ul",null,[s("li",null,"Not exposed via the API."),s("li",null,"Not present in the generated TypeScript view models."),s("li",null,"Not present nor accounted for in the generated C# DTOs."),s("li",null,"Not present in the generated editor or list views.")],-1),C=s("p",null,[n("Note that this only needs to be used on members that are public. Non-public members (including "),s("code",null,"internal"),n(") are always invisible to Coalesce.")],-1),u=s("h2",{id:"example-usage",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#example-usage","aria-hidden":"true"},"#"),n(" Example Usage")],-1),m=s("code",null,"Color",-1),v=s("code",null,"ColorHex",-1),b=D(`

    If no color is saved in the database (the user hasn't picked a color), one is deterministically created.

    public class ApplicationUser
    +{
    +    public int ApplicationUserId { get; set; }
    +
    +    [InternalUse]
    +    public string ColorHex { get; set; }
    +
    +    [NotMapped]
    +    public string Color
    +    {
    +        get => ColorHex ?? GenerateColor(ApplicationUserId).ToRGBHexString();
    +        set => ColorHex = value;
    +    }
    +
    +    public static HSLColor GenerateColor(int? seed = null)
    +    {
    +        var random = seed.HasValue ? new Random(seed.Value) : new Random();
    +        return new HSLColor(random.NextDouble(), random.Next(40, 100) / 100d, random.Next(25, 65) / 100d);
    +    }
    +}
    +
    `,2);function h(E,_){const a=r("RouterLink");return p(),t("div",null,[i,y,d,s("p",null,[n("Effectively, an Internal Use member is invisible to Coalesce. This attribute can be considered a "),l(a,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:e(()=>[n("Security Attribute")]),_:1}),n(".")]),C,u,s("p",null,[n("In this example, "),m,n(" is the property exposed to the API, but "),v,n(" is the property that maps to the database that stores the value. A helper method also exists for the color generation, but needs no attribute to be hidden since methods must be explicitly exposed with "),l(a,{to:"/modeling/model-components/attributes/coalesce.html"},{default:e(()=>[n("[Coalesce]")]),_:1}),n(".")]),b])}const A=o(c,[["render",h],["__file","internal-use.html.vue"]]);export{A as default}; diff --git a/assets/list-text.html.29c3267b.js b/assets/list-text.html.29c3267b.js new file mode 100644 index 000000000..990cbf6da --- /dev/null +++ b/assets/list-text.html.29c3267b.js @@ -0,0 +1,13 @@ +import{_ as s,y as n,z as a,a5 as l}from"./framework.fe9a73df.js";const e={},p=l(`

    [ListText]

    When a textual representation of an object needs to be displayed in the UI, this attribute controls which property will be used. Examples include dropdowns and cells in admin UI tables.

    If this attribute is not used, and a property named Name exists on the model, that property will be used. Otherwise, the primary key will be used.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    public string FirstName { get; set; }
    +
    +    public string LastName { get; set; }
    +
    +    [ListText]
    +    [NotMapped]
    +    public string Name => FirstName + " " + LastName
    +}
    +
    `,5),o=[p];function t(c,r){return n(),a("div",null,o)}const i=s(e,[["render",t],["__file","list-text.html.vue"]]);export{i as default}; diff --git a/assets/list-text.html.7340eedf.js b/assets/list-text.html.7340eedf.js new file mode 100644 index 000000000..12a3a7e08 --- /dev/null +++ b/assets/list-text.html.7340eedf.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-05ce079d","path":"/modeling/model-components/attributes/list-text.html","title":"[ListText]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/list-text.md"}');export{e as data}; diff --git a/assets/list-view-model.html.29d6ec3a.js b/assets/list-view-model.html.29d6ec3a.js new file mode 100644 index 000000000..d2b1a9963 --- /dev/null +++ b/assets/list-view-model.html.29d6ec3a.js @@ -0,0 +1 @@ +import{_ as i,y as n,z as l,X as e,B as t,Q as o,$ as a,P as c}from"./framework.fe9a73df.js";const r={},d=e("h1",{id:"typescript-list-viewmodels",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typescript-list-viewmodels","aria-hidden":"true"},"#"),t(" TypeScript List ViewModels")],-1),h=e("p",null,"This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.",-1),u=e("h2",{id:"vue",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#vue","aria-hidden":"true"},"#"),t(" Vue")],-1),_=e("h2",{id:"knockout",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#knockout","aria-hidden":"true"},"#"),t(" Knockout")],-1);function p(m,f){const s=c("RouterLink");return n(),l("div",null,[d,h,u,e("p",null,[t("See: "),o(s,{to:"/stacks/vue/layers/viewmodels.html"},{default:a(()=>[t("Vue ListViewModels")]),_:1})]),_,e("p",null,[t("See: "),o(s,{to:"/stacks/ko/client/list-view-model.html"},{default:a(()=>[t("Knockout ListViewModels")]),_:1})])])}const v=i(r,[["render",p],["__file","list-view-model.html.vue"]]);export{v as default}; diff --git a/assets/list-view-model.html.80510cd0.js b/assets/list-view-model.html.80510cd0.js new file mode 100644 index 000000000..2a7597095 --- /dev/null +++ b/assets/list-view-model.html.80510cd0.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-32435eb2","path":"/stacks/disambiguation/list-view-model.html","title":"TypeScript List ViewModels","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[]},{"level":2,"title":"Knockout","slug":"knockout","link":"#knockout","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/disambiguation/list-view-model.md"}');export{e as data}; diff --git a/assets/list-view-model.html.c0ac40ce.js b/assets/list-view-model.html.c0ac40ce.js new file mode 100644 index 000000000..8551b647b --- /dev/null +++ b/assets/list-view-model.html.c0ac40ce.js @@ -0,0 +1,13 @@ +import{_ as r,y as d,z as c,X as e,B as t,Q as s,$ as n,a5 as u,P as i}from"./framework.fe9a73df.js";const h={},p=e("h1",{id:"typescript-listviewmodels",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typescript-listviewmodels","aria-hidden":"true"},"#"),t(" TypeScript ListViewModels")],-1),m=e("p",null,"These ListViewModels, like the ViewModels, are dependent on Knockout and are designed to be used directly from Knockout bindings in your HTML.",-1),f={class:"table-of-contents"},g=e("h2",{id:"base-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#base-members","aria-hidden":"true"},"#"),t(" Base Members")],-1),b=e("p",null,[t("The following members are defined on "),e("code",null,"BaseListViewModel<>"),t(" and are available to the ListViewModels for all of your model types:")],-1),_=e("p",null,"Name of the primary key of the model that this list represents.",-1),y=e("p",null,"The collection of items that have been loaded from the server.",-1),w=e("p",null,"Adds a new item to the items collection.",-1),v=e("p",null,"Deletes an item and removes it from the items collection.",-1),D=e("p",null,"An arbitrary URL query string to append to the API call when loading the list of items.",-1),C=e("p",null,"True if the list is loading.",-1),k=e("p",null,"True once the list has been loaded.",-1),S=e("p",null,"Load the list using current parameters for paging, searching, etc Result is placed into the items property.",-1),M=e("p",null,"If a load failed, this is a message about why it failed.",-1),T=e("p",null,"Gets the count of items without getting all the items. Result is placed into the count property.",-1),x=e("p",null,"The result of getCount(), or the total on this page.",-1),L=e("p",null,"Total count of items, even ones that are not on the page.",-1),V=e("p",null,"Change to the next page.",-1),P=e("p",null,"True if there is another page after the current page.",-1),O=e("p",null,"Change to the previous page.",-1),N=e("p",null,"True if there is another page before the current page.",-1),E=e("p",null,"Page number. This can be set to get a new page.",-1),K=e("p",null,"Total page count",-1),I=e("p",null,"Number of items on a page.",-1),F=e("p",null,"Name of a field by which this list will be loaded in ascending order.",-1),B=e("code",null,'"none"',-1),j=e("p",null,"Name of a field by which this list will be loaded in descending order.",-1),A=e("p",null,"Toggles sorting between ascending, descending, and no order on the specified field.",-1),q=e("h2",{id:"model-specific-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#model-specific-members","aria-hidden":"true"},"#"),t(" Model-Specific Members")],-1),R=e("h3",{id:"configuration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#configuration","aria-hidden":"true"},"#"),t(" Configuration")],-1),W=e("h3",{id:"filter-object",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#filter-object","aria-hidden":"true"},"#"),t(" Filter Object")],-1),Q=e("p",null,[t("For each exposed scalar property on the underlying EF POCO, "),e("code",null,"filter"),t(" will have a corresponding property. If the "),e("code",null,"filter"),t(" object is set, requests made to the server to retrieve data will be passed all the values in this object via the URL's query string. These parameters will filter the resulting data to only rows where the parameter values match the row's values. For example, if "),e("code",null,"filter.companyId"),t(" is set to a value, only people from that company will be returned.")],-1),z=u(`
    • Dates with a time component will be matched exactly.
    • Dates with no time component will match any dates that fell on that day.
    • Strings will match exactly unless an asterisk is found, in which case they will be matched with string.StartsWith.
    • Enums will match by string or numeric value. Multiple comma-delimited values will create a filter that will match on any of the provided values.
    • Numeric values will match exactly. Multiple comma-delimited values will create a filter that will match on any of the provided values.

    Example usage:

    var list = new ListViewModels.PersonList();
    +list.filter = { lastName: "Erickson" };
    +list.load();
    +

    Static Method Members

    `,4),J=e("h3",{id:"datasources",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#datasources","aria-hidden":"true"},"#"),t(" DataSources")],-1),G=e("code",null,"ListViewModels.DataSources",-1),U=e("code",null,"ViewModel",-1),H=e("code",null,"ListViewModel",-1),X=e("code",null,"dataSources",-1),$=e("code",null,"dataSource",-1);function Y(Z,ee){const a=i("RouterLink"),l=i("router-link"),o=i("Prop");return d(),c("div",null,[p,e("p",null,[t("In addition to "),s(a,{to:"/stacks/ko/client/view-model.html"},{default:n(()=>[t("TypeScript ViewModels")]),_:1}),t(" for interacting with instances of your data classes in TypeScript, Coalesce will also generated a List ViewModel for loading searched, sorted, paginated data from the server.")]),m,e("nav",f,[e("ul",null,[e("li",null,[s(l,{to:"#base-members"},{default:n(()=>[t("Base Members")]),_:1})]),e("li",null,[s(l,{to:"#model-specific-members"},{default:n(()=>[t("Model-Specific Members")]),_:1}),e("ul",null,[e("li",null,[s(l,{to:"#configuration"},{default:n(()=>[t("Configuration")]),_:1})]),e("li",null,[s(l,{to:"#filter-object"},{default:n(()=>[t("Filter Object")]),_:1})]),e("li",null,[s(l,{to:"#static-method-members"},{default:n(()=>[t("Static Method Members")]),_:1})]),e("li",null,[s(l,{to:"#datasources"},{default:n(()=>[t("DataSources")]),_:1})])])])])]),g,b,s(o,{def:"modelKeyName: string",lang:"ts"}),_,s(o,{def:"includes: string",lang:"ts"}),e("p",null,[t("String that is used to control loading and serialization on the server. See "),s(a,{to:"/concepts/includes.html"},{default:n(()=>[t("Includes String")]),_:1}),t(" for more information.")]),s(o,{def:"items: KnockoutObservableArray",lang:"ts"}),y,s(o,{def:"addNewItem: (): TItem",lang:"ts"}),w,s(o,{def:"deleteItem: (item: TItem): JQueryPromise",lang:"ts"}),v,s(o,{def:"queryString: string",lang:"ts"}),D,s(o,{def:"search: KnockoutObservable",lang:"ts"}),e("p",null,[t("Search criteria for the list. This can be easily bound to with a text box for easy search behavior. See "),s(a,{to:"/modeling/model-components/attributes/search.html"},{default:n(()=>[t("[Search]")]),_:1}),t(" for a detailed look at how searching works in Coalesce.")]),s(o,{def:"isLoading: KnockoutObservable",lang:"ts"}),C,s(o,{def:"isLoaded: KnockoutObservable",lang:"ts"}),k,s(o,{def:"load: (callback?: any): JQueryPromise",lang:"ts"}),S,s(o,{def:"message: KnockoutObservable",lang:"ts"}),M,s(o,{def:"getCount: (callback?: any): JQueryPromise",lang:"ts"}),T,s(o,{def:"count: KnockoutObservable",lang:"ts"}),x,s(o,{def:"totalCount: KnockoutObservable",lang:"ts"}),L,s(o,{def:"nextPage: (): void",lang:"ts"}),V,s(o,{def:"nextPageEnabled: KnockoutComputed",lang:"ts"}),P,s(o,{def:"previousPage: (): void",lang:"ts"}),O,s(o,{def:"previousPageEnabled: KnockoutComputed",lang:"ts"}),N,s(o,{def:"page: KnockoutObservable",lang:"ts"}),E,s(o,{def:"pageCount: KnockoutObservable",lang:"ts"}),K,s(o,{def:"pageSize: KnockoutObservable",lang:"ts"}),I,s(o,{def:"orderBy: KnockoutObservable",lang:"ts"}),F,e("p",null,[t("If set to "),B,t(", default sorting behavior, including behavior defined with use of "),s(a,{to:"/modeling/model-components/attributes/default-order-by.html"},{default:n(()=>[t("[DefaultOrderBy]")]),_:1}),t(" in C# POCOs, is suppressed.")]),s(o,{def:"orderByDescending: KnockoutObservable",lang:"ts"}),j,s(o,{def:"orderByToggle: (field: string): void",lang:"ts"}),A,q,R,s(o,{def:"static coalesceConfig: Coalesce.ListViewModelConfiguration",lang:"ts",id:"member-class-config"}),e("p",null,[t("A static configuration object for configuring all instances of the ListViewModel's type is created. See "),s(a,{to:"/stacks/ko/client/model-config.html"},{default:n(()=>[t("ViewModel Configuration")]),_:1}),t(".")]),s(o,{def:"coalesceConfig: Coalesce.ListViewModelConfiguration",lang:"ts",id:"member-instance-config"}),e("p",null,[t("An per-instance configuration object for configuring each specific ListViewModel instance is created. See "),s(a,{to:"/stacks/ko/client/model-config.html"},{default:n(()=>[t("ViewModel Configuration")]),_:1}),t(".")]),W,s(o,{def:`public filter: { + personId?: string + firstName?: string + lastName?: string + gender?: string + companyId?: string +} = null;`,lang:"ts",id:"code-filter-object"}),Q,e("p",null,[t("These parameters all allow for freeform string values, allowing the server to implement any kind of filtering logic desired. The "),s(a,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:n(()=>[t("Standard Data Source")]),_:1}),t(" will perform the following depending on the property type:")]),z,s(o,{def:`public readonly namesStartingWith = new Person.NamesStartingWith(this); +public static NamesStartingWith = class NamesStartingWith extends Coalesce.ClientMethod { ... };`,lang:"ts",id:"code-static-method-members"}),e("p",null,[t("For each exposed "),s(a,{to:"/modeling/model-components/methods.html#static-methods"},{default:n(()=>[t("Static Method")]),_:1}),t(" on your POCO, the members outlined in "),s(a,{to:"/stacks/ko/client/methods.html"},{default:n(()=>[t("Methods - Generated TypeScript")]),_:1}),t(" will be created.")]),J,s(o,{def:` +public dataSources = ListViewModels.PersonDataSources; +public dataSource: DataSource = new this.dataSources.Default();`,lang:"ts",id:"code-data-source-members"}),e("p",null,[t("For each of the "),s(a,{to:"/modeling/model-components/data-sources.html"},{default:n(()=>[t("Data Sources")]),_:1}),t(" on the class, a corresponding class will be added to a namespace named "),G,t(". This namespace can always be accessed on both "),U,t(" and "),H,t(" instances via the "),X,t(" property, and class instances can be assigned to the "),$,t(" property.")])])}const se=r(h,[["render",Y],["__file","list-view-model.html.vue"]]);export{se as default}; diff --git a/assets/list-view-model.html.c0e8f6d8.js b/assets/list-view-model.html.c0e8f6d8.js new file mode 100644 index 000000000..2fc238346 --- /dev/null +++ b/assets/list-view-model.html.c0e8f6d8.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-30cbf62d","path":"/stacks/ko/client/list-view-model.html","title":"TypeScript ListViewModels","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Base Members","slug":"base-members","link":"#base-members","children":[]},{"level":2,"title":"Model-Specific Members","slug":"model-specific-members","link":"#model-specific-members","children":[{"level":3,"title":"Configuration","slug":"configuration","link":"#configuration","children":[]},{"level":3,"title":"Filter Object","slug":"filter-object","link":"#filter-object","children":[]},{"level":3,"title":"Static Method Members","slug":"static-method-members","link":"#static-method-members","children":[]},{"level":3,"title":"DataSources","slug":"datasources","link":"#datasources","children":[]}]}],"git":{"updatedTime":1654889604000},"filePathRelative":"stacks/ko/client/list-view-model.md"}');export{e as data}; diff --git a/assets/load-from-data-source.html.37349d94.js b/assets/load-from-data-source.html.37349d94.js new file mode 100644 index 000000000..e333257e9 --- /dev/null +++ b/assets/load-from-data-source.html.37349d94.js @@ -0,0 +1,12 @@ +import{_ as o,y as p,z as t,Q as a,X as c,B as s,$ as r,a5 as D,P as n}from"./framework.fe9a73df.js";const i={},d=D(`

    [LoadFromDataSource]

    Specifies that the targeted model instance method should load the instance it is called on from the specified data source when invoked from an API endpoint. By default, the default data source for the model's type will be used.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string FirstName { get; set; }
    +
    +    [Coalesce, LoadFromDataSource(typeof(WithoutCases))]
    +    public void ChangeSpacesToDashesInName()
    +    {
    +        FirstName = FirstName.Replace(" ", "-");
    +    }
    +}
    +

    Properties

    `,5);function y(u,m){const e=n("Prop"),l=n("RouterLink");return p(),t("div",null,[d,a(e,{def:"public Type DataSourceType { get; }",ctor:"1"}),c("p",null,[s("The name of the "),a(l,{to:"/modeling/model-components/data-sources.html"},{default:r(()=>[s("Data Source")]),_:1}),s(" to load the instance object from.")])])}const C=o(i,[["render",y],["__file","load-from-data-source.html.vue"]]);export{C as default}; diff --git a/assets/load-from-data-source.html.ec39c8f4.js b/assets/load-from-data-source.html.ec39c8f4.js new file mode 100644 index 000000000..a892be8cd --- /dev/null +++ b/assets/load-from-data-source.html.ec39c8f4.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-e0635f52","path":"/modeling/model-components/attributes/load-from-data-source.html","title":"[LoadFromDataSource]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/load-from-data-source.md"}');export{e as data}; diff --git a/assets/many-to-many.html.41f6bed5.js b/assets/many-to-many.html.41f6bed5.js new file mode 100644 index 000000000..85755102d --- /dev/null +++ b/assets/many-to-many.html.41f6bed5.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-d6277114","path":"/modeling/model-components/attributes/many-to-many.html","title":"[ManyToMany]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1680212877000},"filePathRelative":"modeling/model-components/attributes/many-to-many.md"}');export{e as data}; diff --git a/assets/many-to-many.html.806b3ca1.js b/assets/many-to-many.html.806b3ca1.js new file mode 100644 index 000000000..cd724a568 --- /dev/null +++ b/assets/many-to-many.html.806b3ca1.js @@ -0,0 +1,10 @@ +import{_ as t,y as p,z as r,X as s,B as n,Q as e,$ as i,a5 as c,P as o}from"./framework.fe9a73df.js";const D={},y=s("h1",{id:"manytomany",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#manytomany","aria-hidden":"true"},"#"),n(" [ManyToMany]")],-1),d=s("p",null,"Used to specify a Many to Many relationship. Because EF core does not support automatic intermediate mapping tables, this field is used to allow for direct reference of the many-to-many collections from the ViewModel.",-1),h=c(`

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string FirstName { get; set; }
    +    public string LastName { get; set; }
    +
    +    [ManyToMany("Appointments")]
    +    public ICollection<PersonAppointment> PersonAppointments { get; set; }
    +}
    +

    Properties

    `,3),m=s("p",null,"The name of the collection that will contain the set of objects on the other side of the many-to-many relationship.",-1),u=s("p",null,"The name of the navigation property on the middle entity that points at the far side of the many-to-many relationship. Use this to resolve ambiguities when the middle table of the many-to-many relationship has more than two reference navigation properties on it.",-1);function C(f,b){const l=o("RouterLink"),a=o("Prop");return p(),r("div",null,[y,d,s("p",null,[n("The named specified in the attribute will be used as the name of a collection of the objects on the other side of the relationship in the generated "),e(l,{to:"/stacks/disambiguation/view-model.html"},{default:i(()=>[n("TypeScript ViewModels")]),_:1}),n(".")]),h,e(a,{def:"public string CollectionName { get; }",ctor:"1"}),m,e(a,{def:"public string FarNavigationProperty { get; set; }"}),u])}const _=t(D,[["render",C],["__file","many-to-many.html.vue"]]);export{_ as default}; diff --git a/assets/metadata.html.23f4eb70.js b/assets/metadata.html.23f4eb70.js new file mode 100644 index 000000000..1717cb863 --- /dev/null +++ b/assets/metadata.html.23f4eb70.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3b29db1e","path":"/stacks/vue/layers/metadata.html","title":"Vue Metadata Layer","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Concepts","slug":"concepts","link":"#concepts","children":[{"level":3,"title":"Metadata","slug":"metadata","link":"#metadata","children":[]},{"level":3,"title":"Type","slug":"type","link":"#type","children":[]},{"level":3,"title":"Value","slug":"value","link":"#value","children":[]},{"level":3,"title":"Property","slug":"property","link":"#property","children":[]},{"level":3,"title":"Domain","slug":"domain","link":"#domain","children":[]}]}],"git":{"updatedTime":1677792094000},"filePathRelative":"stacks/vue/layers/metadata.md"}');export{e as data}; diff --git a/assets/metadata.html.cbe35d07.js b/assets/metadata.html.cbe35d07.js new file mode 100644 index 000000000..3a22517cf --- /dev/null +++ b/assets/metadata.html.cbe35d07.js @@ -0,0 +1 @@ +import{_ as h,y as p,z as u,W as l,X as e,Q as a,$ as o,B as t,a5 as i,P as s}from"./framework.fe9a73df.js";const m={},f=e("h1",{id:"vue-metadata-layer",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#vue-metadata-layer","aria-hidden":"true"},"#"),t(" Vue Metadata Layer")],-1),y=e("p",null,[t("The metadata layer, generated as "),e("code",null,"metadata.g.ts"),t(", contains information about the types, properties, methods, and other components of your data model. Because Vue applications are typically compiled into a set of static assets, it is necessary for the frontend code to have a representation of your data model as an analog to the "),e("code",null,"ReflectionRepository"),t(" that is available at runtime in your .NET app.")],-1),_={class:"table-of-contents"},v=e("h2",{id:"concepts",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#concepts","aria-hidden":"true"},"#"),t(" Concepts")],-1),b={href:"https://github.com/IntelliTect/Coalesce/blob/dev/src/coalesce-vue/src/metadata.ts",target:"_blank",rel:"noopener noreferrer"},g=e("h3",{id:"metadata",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#metadata","aria-hidden":"true"},"#"),t(" Metadata")],-1),x=e("p",null,[t("All objects in the metadata layer that represent any kind of metadata have, at the very least, a "),e("code",null,"name"),t(", the name of the metadata element in code (type names, property names, parameter names, etc). and a "),e("code",null,"displayName"),t(", the human-readable form of the name that is suitable for presentation when needed. Names follow the casing convention of their corresponding language elements - types are PascalCased, while other things like properties, methods, and parameters are camelCased.")],-1),T=e("h3",{id:"type",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#type","aria-hidden":"true"},"#"),t(" Type")],-1),k=e("code",null,"model",-1),C=e("code",null,"object",-1),V=e("h3",{id:"value",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#value","aria-hidden":"true"},"#"),t(" Value")],-1),w=e("p",null,"In the metadata layer, a Value is the usage of a type. This could be any type - strings, numbers, enums, classes, or even void. Values can be found in the collection of an object's properties, a method's parameters or return value, or as a data source's parameters.",-1),N=e("p",null,"All values have the following properties:",-1),R=i("

    Type could be a language primitive like string or number, a non-primitive JavaScript type (date, file), or in the case of a custom Type, the type kind of that type (model, enum, object). For custom types, an additional property typeDef will refer to the Type metadata for that type.

    ",1),E=i('

    Role represents what purpose the value serves in a relational model. Either value (the default - no relational role), primaryKey, foreignKey, referenceNavigation, or collectionNavigation.

    Property

    A Property is a more refined Value that contains a number of additional fields based on the role of the property.

    Domain

    The type of the default export of the generated metadata. Serves as a single root from which all other metadata can be accessed. Contains fields types, enums, and services as organizing structures for the different kinds of custom types.

    ',5);function P(A,M){const n=s("router-link"),c=s("ExternalLinkIcon"),d=s("RouterLink"),r=s("Prop");return p(),u("div",null,[f,l(" MARKER:summary "),y,l(" MARKER:summary-end "),e("nav",_,[e("ul",null,[e("li",null,[a(n,{to:"#concepts"},{default:o(()=>[t("Concepts")]),_:1}),e("ul",null,[e("li",null,[a(n,{to:"#metadata"},{default:o(()=>[t("Metadata")]),_:1})]),e("li",null,[a(n,{to:"#type"},{default:o(()=>[t("Type")]),_:1})]),e("li",null,[a(n,{to:"#value"},{default:o(()=>[t("Value")]),_:1})]),e("li",null,[a(n,{to:"#property"},{default:o(()=>[t("Property")]),_:1})]),e("li",null,[a(n,{to:"#domain"},{default:o(()=>[t("Domain")]),_:1})])])])])]),v,e("p",null,[t("The following is a non-exhaustive list of the general concepts used by the metadata layer. The "),e("a",b,[t("source code of coalesce-vue"),a(c)]),t(" provides the most exhaustive set of documentation about the metadata layer:")]),g,x,T,e("p",null,[t("All custom types exposed by your application's data model will have a Type metadata object generated. This includes both C# classes, and C# enums. Class types include "),k,t(" (for "),a(d,{to:"/modeling/model-types/entities.html"},{default:o(()=>[t("Entity Models")]),_:1}),t(" and "),a(d,{to:"/modeling/model-types/dtos.html"},{default:o(()=>[t("Custom DTOs")]),_:1}),t(") and "),C,t(" (for "),a(d,{to:"/modeling/model-types/external-types.html"},{default:o(()=>[t("External Types")]),_:1}),t(").")]),V,w,N,a(r,{def:"type: TypeDiscriminator",lang:"ts"}),R,a(r,{def:"role: ValueRole",lang:"ts"}),E])}const B=h(m,[["render",P],["__file","metadata.html.vue"]]);export{B as default}; diff --git a/assets/methods.html.2354376c.js b/assets/methods.html.2354376c.js new file mode 100644 index 000000000..ad8ad17c9 --- /dev/null +++ b/assets/methods.html.2354376c.js @@ -0,0 +1,6 @@ +import{_ as c,y as d,z as h,X as e,B as t,Q as s,$ as n,a5 as u,P as a}from"./framework.fe9a73df.js";const p={},m=e("h1",{id:"typescript-method-objects",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typescript-method-objects","aria-hidden":"true"},"#"),t(" TypeScript Method Objects")],-1),b=u(`

    Here's an example for a method called Rename that takes a single parameter 'string name' and returns a string.

    public string Rename(string name)
    +{
    +    FirstName = name;
    +    return FullName; // Return the new full name of the person.
    +}
    +
    `,2),f={class:"table-of-contents"},g=e("h2",{id:"base-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#base-members","aria-hidden":"true"},"#"),t(" Base Members")],-1),_=e("p",null,"The following members are available on the method object for all client methods:",-1),v=e("p",null,"Observable that will contain the results of the method call after it is complete.",-1),w=e("p",null,"Observable with the raw, deserialized JSON result of the method call. If the method call returns an object, this will contain the deserialized JSON object from the server before it has been loaded into ViewModels and its properties loaded into observables.",-1),D=e("p",null,"Observable boolean which is true while the call to the server is pending.",-1),y=e("p",null,"If the method was not successful, this contains exception information.",-1),k=e("p",null,"Observable boolean that indicates whether the method call was successful or not.",-1),O=e("h2",{id:"listresult-t-base-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#listresult-t-base-members","aria-hidden":"true"},"#"),t(),e("code",null,"ListResult"),t(" Base Members")],-1),C=e("p",null,[t("For methods that return a "),e("code",null,"ListResult"),t(", the following additional members on the method object will be available:")],-1),M=e("p",null,"Page number of the results.",-1),T=e("p",null,"Page size of the results.",-1),R=e("p",null,"Total number of possible result pages.",-1),L=e("p",null,"Total number of results.",-1),x=e("h2",{id:"method-specific-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#method-specific-members","aria-hidden":"true"},"#"),t(" Method-specific Members")],-1),j=e("p",null,[t("Function that takes all the method parameters and a callback. If "),e("code",null,"reload"),t(" is true, the ViewModel or ListViewModel that owns the method will be reloaded after the call is complete, and only after that happens will the callback be called.")],-1),P=e("p",null,"Class with one observable member per method argument for binding method arguments to user input. Only generated for methods with arguments.",-1),V=e("p",null,"Default instance of the args class. Only generated for methods with arguments.",-1),A=e("p",null,"Function for invoking the method using the args class. The default instance of the args class will be used if none is provided. Only generated for methods with arguments.",-1),K=e("p",null,[t("Simple interface using browser "),e("code",null,"prompt()"),t(" input boxes to prompt the user for the required data for the method call. The call is then made with the data provided. Only generated for methods with arguments.")],-1),E={href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noopener noreferrer"},F=e("code",null,"src",-1),S=e("code",null,"image",-1),B=e("code",null,"video",-1),N=e("code",null,"this.args",-1);function z(I,U){const o=a("RouterLink"),i=a("router-link"),l=a("Prop"),r=a("ExternalLinkIcon");return d(),h("div",null,[m,e("p",null,[t("For each "),s(o,{to:"/modeling/model-components/methods.html"},{default:n(()=>[t("Custom Method")]),_:1}),t(" you define, a class will be created on the corresponding TypeScript ViewModel (instance methods) or ListViewModel (static methods) that contains the properties and functions for interaction with the method. This class is accessible through a static property named after the method. An instance of this class will also be created on each instance of its parent - this instance is in a property with the camel-cased name of the method.")]),b,e("nav",f,[e("ul",null,[e("li",null,[s(i,{to:"#base-members"},{default:n(()=>[t("Base Members")]),_:1})]),e("li",null,[s(i,{to:"#listresult-t-base-members"},{default:n(()=>[t("ListResult Base Members")]),_:1})]),e("li",null,[s(i,{to:"#method-specific-members"},{default:n(()=>[t("Method-specific Members")]),_:1})])])]),g,_,s(l,{def:"public result: KnockoutObservable",lang:"ts"}),v,s(l,{def:"public rawResult: KnockoutObservable",lang:"ts"}),w,s(l,{def:"public isLoading: KnockoutObservable",lang:"ts"}),D,s(l,{def:"public message: KnockoutObservable",lang:"ts"}),y,s(l,{def:"public wasSuccessful: KnockoutObservable",lang:"ts"}),k,O,C,s(l,{def:"public page: KnockoutObservable",lang:"ts"}),M,s(l,{def:"public pageSize: KnockoutObservable",lang:"ts"}),T,s(l,{def:"public pageCount: KnockoutObservable",lang:"ts"}),R,s(l,{def:"public totalCount: KnockoutObservable",lang:"ts"}),L,x,s(l,{def:"public static Rename = class Rename extends Coalesce.ClientMethod { ... }",lang:"ts",id:"method-object-class-declaration"}),e("p",null,[t("Declaration of the method object class. This will be generated on the parent "),s(o,{to:"/stacks/ko/client/view-model.html"},{default:n(()=>[t("ViewModel")]),_:1}),t(" or "),s(o,{to:"/stacks/ko/client/list-view-model.html"},{default:n(()=>[t("ListViewModel")]),_:1}),t(".")]),s(l,{def:"public readonly rename = new Person.Rename(this)",lang:"ts",id:"method-object-instance"}),e("p",null,[t("Default instance of the method for easy calling of the method without needing to manually instantiate the class. This will be generated on the parent "),s(o,{to:"/stacks/ko/client/view-model.html"},{default:n(()=>[t("ViewModel")]),_:1}),t(" or "),s(o,{to:"/stacks/ko/client/list-view-model.html"},{default:n(()=>[t("ListViewModel")]),_:1}),t(".")]),s(l,{def:"public invoke: (name: string, callback: (result: string) => void = null, reload: boolean = true): JQueryPromise",lang:"ts"}),j,s(l,{def:"public static Args = class Args { public name: KnockoutObservable = ko.observable(null); }",lang:"ts",id:"method-args-class-declaration"}),P,s(l,{def:"public args = new Rename.Args()",lang:"ts",id:"method-args-instance"}),V,s(l,{def:"public invokeWithArgs: (args = this.args, callback?: (result: string) => void, reload: boolean = true) => JQueryPromise",lang:"ts"}),A,s(l,{def:"public invokeWithPrompts: (callback: (result: string) => void = null, reload: boolean = true) => JQueryPromise",lang:"ts"}),K,s(l,{def:"public resultObjectUrl: KnockoutObservable",lang:"ts"}),e("p",null,[t("Observable that will contain an "),e("a",E,[t("Object URL"),s(r)]),t(" representing the last successful invocation result. Only generated for "),s(o,{to:"/modeling/model-components/methods.html#file-downloads"},{default:n(()=>[t("methods that return a file")]),_:1}),t(".")]),s(l,{def:"public url: KnockoutComputed",lang:"ts"}),e("p",null,[t("The URL for the method. Can be useful for using as the "),F,t(" attribute of an "),S,t(" or "),B,t(" HTML element for file-downloading methods. Any arguments will be populated from "),N,t(". Only generated for HTTP GET methods, as configured by "),s(o,{to:"/modeling/model-components/attributes/controller-action.html"},{default:n(()=>[t("[ControllerAction]")]),_:1}),t(".")])])}const Q=c(p,[["render",z],["__file","methods.html.vue"]]);export{Q as default}; diff --git a/assets/methods.html.c31b3de0.js b/assets/methods.html.c31b3de0.js new file mode 100644 index 000000000..882811474 --- /dev/null +++ b/assets/methods.html.c31b3de0.js @@ -0,0 +1,148 @@ +import{_ as i,y as D,z as d,X as s,B as e,Q as n,$ as l,a5 as p,P as r}from"./framework.fe9a73df.js";const y={},u=s("h1",{id:"methods",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#methods","aria-hidden":"true"},"#"),e(" Methods")],-1),h=s("p",null,"These custom methods allow you to implement any custom server-side functionality in your Coalesce application that falls outside of the standard CRUD functions that are generated for your entities.",-1),m={class:"table-of-contents"},C=s("h2",{id:"declaring-methods",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#declaring-methods","aria-hidden":"true"},"#"),e(" Declaring Methods")],-1),v=s("h3",{id:"instance-methods",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#instance-methods","aria-hidden":"true"},"#"),e(" Instance Methods")],-1),b=p(`
    public class User
    +{
    +    public int UserId { get; set; }
    +
    +    public string Email { get; set; }
    +
    +    [Coalesce]
    +    public async Task<ItemResult> SendMessage(
    +        [Inject] SmtpClient client,
    +        ClaimsPrincipal sender,
    +        string message
    +    ) {
    +        if (string.IsNullOrWhitespace(Email)) return "Recipient has no email";
    +        if (string.IsNullOrWhitespace(message)) return "Message is required";
    +
    +        await client.SendMailAsync(new MailMessage(  
    +            from: sender.GetEmailAddress(),
    +            to: Email,
    +            subject: "Message from MyApp",  
    +            body: message
    +        ));
    +        return true;
    +    }
    +}
    +
    +
    `,1),f=s("code",null,"[LoadFromDataSource(typeof(MyDataSource))]",-1),g=s("code",null,"[DefaultDataSource]",-1),_=p('

    Instance methods are generated onto the TypeScript ViewModels.

    When should I use Instance Methods?

    Instance methods, as opposed to static or service methods, are a good fit when implementing an action that directly acts on or depends upon a specific instance of one of your entity types. One of their biggest benefits is the automatic row-level security from data sources as described above.

    Static Methods

    ',4),E=p(`
    public class Person 
    +{
    +    public int PersonId { get; set; }
    +
    +    public string FirstName { get; set; }
    +
    +    [Coalesce]
    +    public static ICollection<string> NamesStartingWith(
    +        AppDbContext db,
    +        string characters 
    +    ) {
    +        return db.People
    +            .Select(p => p.FirstName)
    +            .Where(f => f.StartsWith(characters))
    +            .ToList();
    +    }
    +}
    +

    Static methods are generated onto the TypeScript ListViewModels. All of the same members that are generated for instance methods are also generated for static methods.

    When should I use Static Methods?

    Static methods are a good fit for actions that don't operate on a specific instance of an entity type, but whose functionality is still closely coupled with a specific, concrete entity type.

    `,4),w=s("code",null,"/save",-1),F=s("p",null,"Or, imagine an Invoice class. You might make a static method that returns a summary of sales information for a given time range. Since this summarization would be performing aggregate functions against your Invoice entities and is therefore tightly coupled to Invoices, a static method would be suitable.",-1),A=s("h3",{id:"service-methods",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#service-methods","aria-hidden":"true"},"#"),e(" Service Methods")],-1),T=p(`
    [Coalesce, Service]
    +public class MyService 
    +{
    +  [Coalesce]
    +  public string MyServiceMethod() => "Hello, World!";
    +}
    +
    `,1),I=s("em",null,"interface",-1),k=p(`
    [Coalesce, Service]
    +public interface IMyService 
    +{
    +  string MyServiceMethod() => "Hello, World!";
    +}
    +

    When declaring service methods by interface, a [Coalesce] attribute on each method is not needed - the entire interface is exposed by Coalesce.

    When should I use Service Methods?

    `,3),x=p('

    However, there are some reasons why you might not want to use a service:

    • If the method logically operates on a single entity instance, and/or if using an instance method would let you utilize the row-level security already implemented by one of your data sources to authorize who can invoke the method.
    • If the service would only have one or two methods and would logically make sense as a static or instance method. In other words, if adding a new service class would be detrimental to the organization of your codebase and create "file sprawl".

    On the other hand, services have some benefits that instance and static methods cannot provide:

    • Coalesce Services can be declared with an interface, rather than a concrete type, allowing for their implementation to be substituted more easily. For example, a service providing an external integration that you want to mock or stub during automated testing and/or local development.

    Parameters

    The following parameters can be added to your methods:

    ',6),M=s("thead",null,[s("tr",null,[s("td",null,"Type"),s("td",null,"Description")])],-1),S=s("tr",null,[s("td",null,[s("p",null,"Primitives, Dates, and other Scalars")]),s("td",null,[s("p",null,[e("Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, "),s("code",null,"DateTime"),e(", "),s("code",null,"DateTimeOffset"),e("), and their nullable variants, are accepted as parameters to be passed from the client to the method call.")])])],-1),B=s("td",null,[s("p",null,"When invoking the method on the client, the object's properties will only be serialized one level deep. If an entity model parameter has additional child object properties, they will not be included in the invocation of the method - only the object's primitive & date properties will be deserialized from the client.")],-1),P=s("td",null,[s("p",null,"Unlike entity model parameters, external type parameters will be serialized and sent by the client to an arbitrarily deep level, excluding any entity model properties that may be nested inside an external type.")],-1),R=s("tr",null,[s("td",null,[s("p",null,"Files")]),s("td",null,[s("p",null,[e("Methods can accept file uploads by using a parameter of type "),s("code",null,"IntelliTect.Coalesce.Models.IFile"),e(" (or any derived type, like "),s("code",null,"IntelliTect.Coalesce.Models.File"),e(").")])])],-1),q={href:"https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.icollection-1",target:"_blank",rel:"noopener noreferrer"},L={href:"https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1",target:"_blank",rel:"noopener noreferrer"},j=s("td",null,[s("p",null,"Collections of any of the above valid parameter types above are also valid parameter types.")],-1),O={href:"https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext",target:"_blank",rel:"noopener noreferrer"},W=s("td",null,[s("p",null,[e("If the method has a parameter assignable to "),s("code",null,"Microsoft.EntityFrameworkCore.DbContext"),e(", then the parameter will be implicitly "),s("code",null,"[Inject]"),e("ed.")])],-1),H={href:"https://learn.microsoft.com/en-us/dotnet/api/system.security.claims.claimsprincipal",target:"_blank",rel:"noopener noreferrer"},U=s("td",null,[s("p",null,[e("If the method has a parameter of type ClaimsPrincipal, the value of "),s("code",null,"HttpContext.User"),e(" will be passed to the parameter.")])],-1),z=s("code",null,"IServiceProvider",-1),V=s("code",null,"out",-1),N=s("code",null,"ItemResult",-1),G=s("code",null,"IncludeTree",-1),Q=s("code",null,"ItemResult",-1),Y=s("h2",{id:"return-values",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#return-values","aria-hidden":"true"},"#"),e(" Return Values")],-1),$=s("p",null,"You can return virtually anything from these methods:",-1),K=s("thead",null,[s("tr",null,[s("td",null,"Type"),s("td",null,"Description")])],-1),X=s("tr",null,[s("td",null,[s("p",null,"Primitives, Dates, and other Scalars")]),s("td",null,[s("p",null,[e("Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, "),s("code",null,"DateTime"),e(", "),s("code",null,"DateTimeOffset"),e("), and their nullable variants, may be returned from methods.")])])],-1),J=s("td",null,[s("p",null,"Any of the types of your models may be returned.")],-1),Z=s("strong",null,"recursively",-1),ss={href:"https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.icollection-1",target:"_blank",rel:"noopener noreferrer"},es={href:"https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1",target:"_blank",rel:"noopener noreferrer"},ns=s("td",null,[s("p",null,[e("Collections of any of the above valid return types above are also valid return types. IEnumerables are useful for generator functions using "),s("code",null,"yield"),e(". "),s("code",null,"ICollection"),e(" is highly suggested over "),s("code",null,"IEnumerable"),e(" whenever appropriate, though.")])],-1),ls={href:"https://learn.microsoft.com/en-us/dotnet/api/system.linq.iqueryable",target:"_blank",rel:"noopener noreferrer"},as=s("code",null,"IQueryable<>",-1),os=s("code",null,"ItemResult",-1),ts=s("code",null,"IncludeTree",-1),ps=s("tr",null,[s("td",null,[s("p",null,[s("a",{href:"#file-downloads"},"Files")])]),s("td",null,[s("p",null,[e("Methods can return file downloads using type "),s("code",null,"IntelliTect.Coalesce.Models.IFile"),e(" (or any derived type, like "),s("code",null,"IntelliTect.Coalesce.Models.File"),e(").")]),s("p",null,[e("Please see the "),s("a",{href:"#file-downloads"},"File Downloads"),e(" section below for more details")])])],-1),rs=s("td",null,[s("p",null,[s("code",null,"ItemResult"),e(", "),s("code",null,"ItemResult"),e(", "),s("code",null,"ListResult")])],-1),cs=p("

    An IntelliTect.Coalesce.Models.ItemResult<T> of any of the valid return types above, including collections, is valid, as well as its non-generic variant ItemResult, and its list variant ListResult<T>.

    Use an ItemResult whenever you might need to signal failure and return an error message from a custom method. The WasSuccessful and Message properties on the result object will be sent along to the client to indicate success or failure of the method. The type T will be mapped to the appropriate DTO object before being serialized as normal.

    ",2),is=s("code",null,"IncludeTree",-1),Ds=s("h2",{id:"security",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#security","aria-hidden":"true"},"#"),e(" Security")],-1),ds=s("p",null,"Security for instance methods is also controlled by the data source that loads the instance - if the data source can't provide an instance of the requested model, the method won't be executed.",-1),ys=s("h2",{id:"generated-typescript",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#generated-typescript","aria-hidden":"true"},"#"),e(" Generated TypeScript")],-1),us=s("div",{class:"custom-container tip"},[s("p",{class:"custom-container-title"},"Note"),s("p",null,`Any Task-returning methods with "Async" as a suffix to the C# method's name will have the "Async" suffix stripped from the generated Typescript.`)],-1),hs=s("h2",{id:"method-annotations",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#method-annotations","aria-hidden":"true"},"#"),e(" Method Annotations")],-1),ms=s("h3",{id:"coalesce",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#coalesce","aria-hidden":"true"},"#"),e(),s("code",null,"[Coalesce]")],-1),Cs=s("code",null,"[Service]",-1),vs=s("h3",{id:"controlleraction-method-httpmethod-varybyproperty-string",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#controlleraction-method-httpmethod-varybyproperty-string","aria-hidden":"true"},"#"),e(),s("code",null,"[ControllerAction(Method = HttpMethod, VaryByProperty = string)]")],-1),bs=s("h3",{id:"execute-string-roles",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#execute-string-roles","aria-hidden":"true"},"#"),e(),s("code",null,"[Execute(string roles)]")],-1),fs=s("h3",{id:"loadfromdatasource-type-datasourcetype",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#loadfromdatasource-type-datasourcetype","aria-hidden":"true"},"#"),e(),s("code",null,"[LoadFromDataSource(Type dataSourceType)]")],-1),gs=p('

    File Downloads

    Coalesce supports exposing file downloads via custom methods. Simply return a IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File), or an ItemResult<> of such.

    Consuming file downloads

    There are a few conveniences for easily consuming downloaded files from your custom pages.

    ',4),_s=s("code",null,"url",-1),Es=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),ws=s("div",{class:"language-html line-numbers-mode","data-ext":"html"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},":src"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"downloadPicture.url"'),s("span",{style:{color:"#808080"}},">")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"})])],-1),Fs=s("hr",null,null,-1),As=s("code",null,"getResultObjectUrl(vue)",-1),Ts=s("code",null,"caller()",-1),Is=s("code",null,"caller.invoke()",-1),ks=s("code",null,"caller.invokeWithArgs()",-1),xs={href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noopener noreferrer"},Ms=s("code",null,"src",-1),Ss=s("code",null,"image",-1),Bs=s("code",null,"video",-1),Ps=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"import"),s("span",{style:{color:"#D4D4D4"}}," { "),s("span",{style:{color:"#9CDCFE"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}}," } "),s("span",{style:{color:"#C586C0"}},"from"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#CE9178"}},"'@/viewmodels.g'")]),e(` +`),s("span",{class:"line"}),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#DCDCAA"}},"PersonViewModel"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"$load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#C586C0"}},"await"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"downloadPicture"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),Rs=s("div",{class:"language-html line-numbers-mode","data-ext":"html"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},":src"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"downloadPicture.getResultObjectUrl()"'),s("span",{style:{color:"#808080"}},">")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"})])],-1),qs=s("code",null,"url",-1),Ls=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},");")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),js=s("div",{class:"language-html line-numbers-mode","data-ext":"html"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"data-bind"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"attr: {src: downloadPicture.url }"'),s("span",{style:{color:"#808080"}},">")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"})])],-1),Os=s("hr",null,null,-1),Ws=s("code",null,"resultObjectUrl",-1),Hs=s("code",null,".invoke()",-1),Us=s("code",null,".invokeWithArgs()",-1),zs={href:"https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL",target:"_blank",rel:"noopener noreferrer"},Vs=s("code",null,"src",-1),Ns=s("code",null,"image",-1),Gs=s("code",null,"video",-1),Qs=s("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#569CD6"}},"var"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}}," = "),s("span",{style:{color:"#569CD6"}},"new"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"ViewModels"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"Person"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"load"),s("span",{style:{color:"#D4D4D4"}},"("),s("span",{style:{color:"#B5CEA8"}},"1"),s("span",{style:{color:"#D4D4D4"}},", () "),s("span",{style:{color:"#569CD6"}},"=>"),s("span",{style:{color:"#D4D4D4"}}," {")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"viewModel"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#9CDCFE"}},"downloadPicture"),s("span",{style:{color:"#D4D4D4"}},"."),s("span",{style:{color:"#DCDCAA"}},"invoke"),s("span",{style:{color:"#D4D4D4"}},"();")]),e(` +`),s("span",{class:"line"},[s("span",{style:{color:"#D4D4D4"}},"});")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"}),s("div",{class:"line-number"})])],-1),Ys=s("div",{class:"language-html line-numbers-mode","data-ext":"html"},[s("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[s("code",null,[s("span",{class:"line"},[s("span",{style:{color:"#808080"}},"<"),s("span",{style:{color:"#569CD6"}},"img"),s("span",{style:{color:"#D4D4D4"}}," "),s("span",{style:{color:"#9CDCFE"}},"data-bind"),s("span",{style:{color:"#D4D4D4"}},"="),s("span",{style:{color:"#CE9178"}},'"attr: {src: downloadPicture.resultObjectUrl }"'),s("span",{style:{color:"#808080"}},">")]),e(` +`),s("span",{class:"line"})])]),s("div",{class:"line-numbers","aria-hidden":"true"},[s("div",{class:"line-number"})])],-1),$s=s("h3",{id:"database-stored-files",tabindex:"-1"},[s("a",{class:"header-anchor",href:"#database-stored-files","aria-hidden":"true"},"#"),e(" Database-stored Files")],-1),Ks=s("code",null,"byte[]",-1),Xs={href:"https://github.com/dotnet/SqlClient/issues/593",target:"_blank",rel:"noopener noreferrer"},Js={href:"https://docs.microsoft.com/en-us/ef/core/modeling/table-splitting",target:"_blank",rel:"noopener noreferrer"},Zs=p(`

    WARNING

    Storing large binary objects in relational databases comes with significant drawbacks. For large-volume cloud solutions, it is much more costly than dedicated cloud-native file storage like Azure Storage or S3. Also of note is that the larger a database is, the more difficult its backup process becomes.

    For files that are stored in your database, Coalesce supports a pattern that allows the file to be streamed directly to the HTTP response without needing to allocate a chunk of memory for the whole file at once. Simply pass an EF IQueryable<byte[]> to the constructor of IntelliTect.Coalesce.Models.File. This implementation, however, is specific to the underlying EF database provider. Currently, only SQL Server and SQLite are supported. Please open a Github issue to request support for other providers. An example of this mechanism is included in the DownloadAttachment method in the code sample below.

    The following is an example of utilizing Table Splitting for database-stored files. Generally speaking, metadata about the file should be stored on the "main" entity, and only the bytes of the content should be split into a separate entity.

    public class AppDbContext : DbContext
    +{
    +    public DbSet<Case> Cases { get; set; }
    +
    +    protected override void OnModelCreating(ModelBuilder modelBuilder)
    +    {
    +        modelBuilder
    +            .Entity<Case>()
    +            .ToTable("Cases")
    +            .HasOne(c => c.AttachmentContent)
    +            .WithOne()
    +            .HasForeignKey<CaseAttachmentContent>(c => c.CaseId);
    +        modelBuilder
    +            .Entity<CaseAttachmentContent>()
    +            .ToTable("Cases")
    +            .HasKey(d => d.CaseId);
    +    }
    +}
    +
    +public class Case
    +{
    +    public int CaseId { get; set; }
    +
    +    [Read]
    +    public string AttachmentName { get; set; }
    +
    +    [Read]
    +    public long AttachmentSize { get; set; }
    +
    +    [Read]
    +    public string AttachmentType { get; set; }
    +
    +    [Read, MaxLength(32)] // Adjust max length based on chosen hash algorithm.
    +    public byte[] AttachmentHash { get; set; } // Could also be a base64 string if so desired.
    +
    +    [InternalUse]
    +    public CaseAttachmentContent AttachmentContent { get; set; } = new();
    +
    +    [Coalesce]
    +    public async Task UploadAttachment(AppDbContext db, IFile file)
    +    {
    +        if (file.Content == null) return;
    +
    +        var content = new byte[file.Length];
    +        await file.Content.ReadAsync(content.AsMemory());
    +
    +        AttachmentContent = new () { CaseId = CaseId, Content = content };
    +        AttachmentName = file.Name;
    +        AttachmentSize = file.Length;
    +        AttachmentType = file.ContentType;
    +        AttachmentHash = SHA256.HashData(content);
    +    }
    +
    +    [Coalesce]
    +    [ControllerAction(HttpMethod.Get, VaryByProperty = nameof(AttachmentHash))]
    +    public IFile DownloadAttachment(AppDbContext db)
    +    {
    +        return new IntelliTect.Coalesce.Models.File(db.Cases
    +            .Where(c => c.CaseId == this.CaseId)
    +            .Select(c => c.AttachmentContent.Content)
    +        )
    +        {
    +            Name = AttachmentName,
    +            ContentType = AttachmentType,
    +        };
    +    }
    +}
    +
    +public class CaseAttachmentContent
    +{
    +    public int CaseId { get; set; }
    +
    +    [Required]
    +    public byte[] Content { get; set; }
    +}
    +

    Other File Storage

    For any other storage mechanism, implementations are similar to the database storage approach above. However, instead of table splitting or using a whole separate table, the file contents are simply stored elsewhere. Continue storing metadata about the file on the primary entity, and implement upload/download methods as desired that wrap the storage provider.

    For downloads, prefer directly providing the underlying Stream to the IFile versus wrapping a byte[] in a MemoryStream. This will reduce server memory usage and garbage collector churn.

    For cloud storage providers where complex security logic is not needed, consider having clients consume the URL of the cloud resource directly rather than passing the file content through your own server.

    `,8);function se(ee,ne){const a=r("RouterLink"),o=r("router-link"),t=r("ExternalLinkIcon"),c=r("CodeTabs");return D(),d("div",null,[u,s("p",null,[e("Any public methods annotated with the "),n(a,{to:"/modeling/model-components/attributes/coalesce.html"},{default:l(()=>[e("[Coalesce]")]),_:1}),e(" attribute that are placed on your model classes will have API endpoints and Typescript generated by Coalesce. Both instance methods and static methods are supported. Additionally, any instance methods on "),n(a,{to:"/modeling/model-types/services.html"},{default:l(()=>[e("Services")]),_:1}),e(" will also have API endpoints and TypeScript generated.")]),h,s("nav",m,[s("ul",null,[s("li",null,[n(o,{to:"#declaring-methods"},{default:l(()=>[e("Declaring Methods")]),_:1}),s("ul",null,[s("li",null,[n(o,{to:"#instance-methods"},{default:l(()=>[e("Instance Methods")]),_:1})]),s("li",null,[n(o,{to:"#static-methods"},{default:l(()=>[e("Static Methods")]),_:1})]),s("li",null,[n(o,{to:"#service-methods"},{default:l(()=>[e("Service Methods")]),_:1})])])]),s("li",null,[n(o,{to:"#parameters"},{default:l(()=>[e("Parameters")]),_:1})]),s("li",null,[n(o,{to:"#return-values"},{default:l(()=>[e("Return Values")]),_:1})]),s("li",null,[n(o,{to:"#security"},{default:l(()=>[e("Security")]),_:1})]),s("li",null,[n(o,{to:"#generated-typescript"},{default:l(()=>[e("Generated TypeScript")]),_:1})]),s("li",null,[n(o,{to:"#method-annotations"},{default:l(()=>[e("Method Annotations")]),_:1}),s("ul",null,[s("li",null,[n(o,{to:"#coalesce"},{default:l(()=>[e("[Coalesce]")]),_:1})]),s("li",null,[n(o,{to:"#controlleraction-method-httpmethod-varybyproperty-string"},{default:l(()=>[e("[ControllerAction(Method = HttpMethod, VaryByProperty = string)]")]),_:1})]),s("li",null,[n(o,{to:"#execute-string-roles"},{default:l(()=>[e("[Execute(string roles)]")]),_:1})]),s("li",null,[n(o,{to:"#loadfromdatasource-type-datasourcetype"},{default:l(()=>[e("[LoadFromDataSource(Type dataSourceType)]")]),_:1})])])]),s("li",null,[n(o,{to:"#file-downloads"},{default:l(()=>[e("File Downloads")]),_:1}),s("ul",null,[s("li",null,[n(o,{to:"#consuming-file-downloads"},{default:l(()=>[e("Consuming file downloads")]),_:1})]),s("li",null,[n(o,{to:"#database-stored-files"},{default:l(()=>[e("Database-stored Files")]),_:1})]),s("li",null,[n(o,{to:"#other-file-storage"},{default:l(()=>[e("Other File Storage")]),_:1})])])])])]),C,v,s("p",null,[e("Instance Methods can be declared on your "),n(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[e("Entity classes")]),_:1}),e(". For example:")]),b,s("p",null,[e("When an instance method is invoked, the target model instance will be loaded using the data source specified by an attribute "),f,e(" if present. Otherwise, the model instance will be loaded using the default data source for the model's type. If you have a "),n(a,{to:"/modeling/model-components/data-sources.html#defining-data-sources"},{default:l(()=>[e("Custom Data Source")]),_:1}),e(" annotated with "),g,e(", that data source will be used. Otherwise, the "),n(a,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[e("Standard Data Source")]),_:1}),e(" will be used. The consequence of this is that a user cannot call a method on an instance of entity that they're not allowed to see or load.")]),_,s("p",null,[e("Static Methods can be declared on your "),n(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[e("Entity classes")]),_:1}),e(". For example:")]),E,s("p",null,[e("For example, imagine you have a File entity class. You could make a static method on that class that accepts a file as a parameter. This method would persist that file to storage and then save a new entity to the database. You would then "),n(a,{to:"/topics/security.html#endpoint-security"},{default:l(()=>[e("disable Create")]),_:1}),e(" on that entity, since the default "),w,e(" endpoint cannot accept file uploads.")]),F,A,s("p",null,[e("Service methods can be declared on a "),n(a,{to:"/modeling/model-types/services.html"},{default:l(()=>[e("Coalesce Service")]),_:1}),e(" class:")]),T,s("p",null,[e("Or, they can be declared via a "),n(a,{to:"/modeling/model-types/services.html"},{default:l(()=>[e("Coalesce Service")]),_:1}),e(),I,e(" that has an implementation registered with dependency injection:")]),k,s("p",null,[n(a,{to:"/modeling/model-types/services.html"},{default:l(()=>[e("Services")]),_:1}),e(" are a catch-all feature and can be used for almost any conceivable purpose in Coalesce to implement custom functionality that needs to be invoked by your front-end app.")]),x,s("table",null,[M,S,s("tr",null,[s("td",null,[s("p",null,[n(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[e("Entity Models")]),_:1})])]),B]),s("tr",null,[s("td",null,[s("p",null,[n(a,{to:"/modeling/model-types/external-types.html"},{default:l(()=>[e("External Types")]),_:1})])]),P]),R,s("tr",null,[s("td",null,[s("p",null,[s("a",q,[e("ICollection"),n(t)]),e(", "),s("a",L,[e("IEnumerable"),n(t)])])]),j]),s("tr",null,[s("td",null,[s("p",null,[s("a",O,[e("DbContext"),n(t)])])]),W]),s("tr",null,[s("td",null,[s("p",null,[s("a",H,[e("ClaimsPrincipal"),n(t)])])]),U]),s("tr",null,[s("td",null,[s("p",null,[n(a,{to:"/modeling/model-components/attributes/inject.html"},{default:l(()=>[e("[Inject]")]),_:1})])]),s("td",null,[s("p",null,[e("If a parameter is marked with the "),n(a,{to:"/modeling/model-components/attributes/inject.html"},{default:l(()=>[e("[Inject]")]),_:1}),e(" attribute, it will be injected from the application's "),z,e(".")])])]),s("tr",null,[s("td",null,[s("p",null,[V,e(),n(a,{to:"/concepts/include-tree.html"},{default:l(()=>[e("IncludeTree")]),_:1})])]),s("td",null,[s("p",null,[e("Deprecated. If you need to return an "),n(a,{to:"/concepts/include-tree.html"},{default:l(()=>[e("Include Tree")]),_:1}),e(" to shape the serialization of the method's return value, you should use an "),N,e(" return value and populate the "),G,e(" property on the "),Q,e(" object.")])])])]),Y,$,s("table",null,[K,X,s("tr",null,[s("td",null,[s("p",null,[n(a,{to:"/modeling/model-types/entities.html"},{default:l(()=>[e("Entity Models")]),_:1})])]),J]),s("tr",null,[s("td",null,[s("p",null,[n(a,{to:"/modeling/model-types/external-types.html"},{default:l(()=>[e("External Types")]),_:1})])]),s("td",null,[s("p",null,[e("Any "),n(a,{to:"/modeling/model-types/external-types.html"},{default:l(()=>[e("External Types")]),_:1}),e(" you define may also be returned from a method.")]),s("p",null,[e("When returning custom types from methods, be careful of the types of their properties. Coalesce will "),Z,e(" discover and generate code for all public properties of your "),n(a,{to:"/modeling/model-types/external-types.html"},{default:l(()=>[e("External Types")]),_:1}),e(". If you accidentally include a type that you do not own, these generated types could get out of hand extremely quickly.")]),s("p",null,[e("Mark any properties you don't want generated with the "),n(a,{to:"/modeling/model-components/attributes/internal-use.html"},{default:l(()=>[e("[InternalUse]")]),_:1}),e(" attribute, or give them a non-public access modifier. Whenever possible, don't return types that you don't own or control.")])])]),s("tr",null,[s("td",null,[s("p",null,[s("a",ss,[e("ICollection"),n(t)]),e(", "),s("a",es,[e("IEnumerable"),n(t)])])]),ns]),s("tr",null,[s("td",null,[s("p",null,[s("a",ls,[e("IQueryable"),n(t)])])]),s("td",null,[s("p",null,[e("Queryables of the valid return types above are valid return types. The query will be evaluated, and Coalesce will attempt to pull an "),n(a,{to:"/concepts/include-tree.html"},{default:l(()=>[e("Include Tree")]),_:1}),e(" from the queryable to shape the response.")]),s("p",null,[e("When "),n(a,{to:"/concepts/include-tree.html"},{default:l(()=>[e("Include Tree")]),_:1}),e(" functionality is needed to shape the response but an "),as,e(" return type is not feasible, an "),os,e(" return value with an "),ts,e(" set on it will do the trick as well.")])])]),ps,s("tr",null,[rs,s("td",null,[cs,s("p",null,[e("An "),n(a,{to:"/concepts/include-tree.html"},{default:l(()=>[e("Include Tree")]),_:1}),e(" can be set on the object's "),is,e(" parameter to shape the serialization of the method's returned value.")])])])]),Ds,s("p",null,[e("You can implement role-based security on a method by placing the "),n(a,{to:"/modeling/model-components/attributes/execute.html"},{default:l(()=>[e("[Execute]")]),_:1}),e(" on the method. Placing this attribute on the method with no roles specified will simply require that the calling user be authenticated.")]),ds,s("p",null,[e("See the "),n(a,{to:"/topics/security.html#custom-methods-and-services"},{default:l(()=>[e("Security")]),_:1}),e(" page to read more about custom method security, as well as all other security mechanisms in Coalesce.")]),ys,s("p",null,[e("See "),n(a,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:l(()=>[e("API Callers")]),_:1}),e(" and "),n(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:l(()=>[e("ViewModel Layer")]),_:1}),e(" for details on the code that is generated for your custom methods.")]),us,hs,s("p",null,[e("Methods can be annotated with attributes to control API exposure and TypeScript generation. The following attributes are available for model methods. General annotations can be found on the "),n(a,{to:"/modeling/model-components/attributes.html"},{default:l(()=>[e("Attributes")]),_:1}),e(" page.")]),ms,s("p",null,[e("The "),n(a,{to:"/modeling/model-components/attributes/coalesce.html"},{default:l(()=>[e("[Coalesce]")]),_:1}),e(" attribute causes the method to be exposed via a generated API controller. This is not needed for methods defined on an interface marked with "),Cs,e(" - Coalesce assumes that all methods on the interface are intended to be exposed. If this is not desired, create a new, more restricted interface with only the desired methods to be exposed.")]),vs,s("p",null,[e("The "),n(a,{to:"/modeling/model-components/attributes/controller-action.html"},{default:l(()=>[e("[ControllerAction]")]),_:1}),e(" attribute controls how this method is exposed via HTTP. Can be used to customize the HTTP method/verb for the method, as well as caching behavior.")]),bs,s("p",null,[e("The "),n(a,{to:"/modeling/model-components/attributes/execute.html"},{default:l(()=>[e("[Execute]")]),_:1}),e(" attribute specifies which roles can execute this method from the generated API controller.")]),fs,s("p",null,[e("The "),n(a,{to:"/modeling/model-components/attributes/load-from-data-source.html"},{default:l(()=>[e("[LoadFromDataSource]")]),_:1}),e(" attribute specifies that the targeted model instance method should load the instance it is called on from the specified data source when invoked from an API endpoint. By default, the default data source for the model's type will be used.")]),gs,n(c,null,{vue:l(()=>[s("p",null,[e("The "),n(a,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:l(()=>[e("API Callers")]),_:1}),e(" have a property "),_s,e(". This can be provided directly to your HTML template, with the browser invoking the endpoint automatically.")]),Es,ws,Fs,s("p",null,[e("Alternatively, the "),n(a,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:l(()=>[e("API Callers")]),_:1}),e(" for file-returning methods have a method "),As,e(". If the method was invoked programmatically (i.e. via "),Ts,e(", "),Is,e(", or "),ks,e("), this method returns an "),s("a",xs,[e("Object URL"),n(t)]),e(" that can be set as the "),Ms,e(" of an "),Ss,e(" or "),Bs,e(" HTML tag.")]),Ps,Rs]),knockout:l(()=>[s("p",null,[e("The "),n(a,{to:"/stacks/ko/client/methods.html"},{default:l(()=>[e("TypeScript Method Objects")]),_:1}),e(" for HTTP GET methods have a property "),qs,e(". This can be provided directly to your HTML, with the browser invoking the endpoint as normal.")]),Ls,js,Os,s("p",null,[e("Alternatively, the "),n(a,{to:"/stacks/ko/client/methods.html"},{default:l(()=>[e("TypeScript Method Objects")]),_:1}),e(" for file-returning methods have a property "),Ws,e(". If the method is invoked programmatically (i.e. via "),Hs,e(" or "),Us,e("), this property contains an "),s("a",zs,[e("Object URL"),n(t)]),e(" that can be set as the "),Vs,e(" of an "),Ns,e(" or "),Gs,e(" HTML tag.")]),Qs,Ys]),_:1}),$s,s("p",null,[e("When storing large "),Ks,e(" objects in your EF models, it is important that these are never loaded unless necessary. Loading these can cause significant garbage collector churn, or even "),s("a",Xs,[e("bring your app to a halt"),n(t)]),e(". To achieve this with EF, you can either utilize "),s("a",Js,[e("Table Splitting"),n(t)]),e(", or you can use an entire dedicated table that only contains a primary key and the binary content, and nothing else.")]),Zs])}const ae=i(y,[["render",se],["__file","methods.html.vue"]]);export{ae as default}; diff --git a/assets/methods.html.cb5a7d01.js b/assets/methods.html.cb5a7d01.js new file mode 100644 index 000000000..6fb422fdd --- /dev/null +++ b/assets/methods.html.cb5a7d01.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0f1d24df","path":"/modeling/model-components/methods.html","title":"Methods","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Declaring Methods","slug":"declaring-methods","link":"#declaring-methods","children":[{"level":3,"title":"Instance Methods","slug":"instance-methods","link":"#instance-methods","children":[]},{"level":3,"title":"Static Methods","slug":"static-methods","link":"#static-methods","children":[]},{"level":3,"title":"Service Methods","slug":"service-methods","link":"#service-methods","children":[]}]},{"level":2,"title":"Parameters","slug":"parameters","link":"#parameters","children":[]},{"level":2,"title":"Return Values","slug":"return-values","link":"#return-values","children":[]},{"level":2,"title":"Security","slug":"security","link":"#security","children":[]},{"level":2,"title":"Generated TypeScript","slug":"generated-typescript","link":"#generated-typescript","children":[]},{"level":2,"title":"Method Annotations","slug":"method-annotations","link":"#method-annotations","children":[{"level":3,"title":"[Coalesce]","slug":"coalesce","link":"#coalesce","children":[]},{"level":3,"title":"[ControllerAction(Method = HttpMethod, VaryByProperty = string)]","slug":"controlleraction-method-httpmethod-varybyproperty-string","link":"#controlleraction-method-httpmethod-varybyproperty-string","children":[]},{"level":3,"title":"[Execute(string roles)]","slug":"execute-string-roles","link":"#execute-string-roles","children":[]},{"level":3,"title":"[LoadFromDataSource(Type dataSourceType)]","slug":"loadfromdatasource-type-datasourcetype","link":"#loadfromdatasource-type-datasourcetype","children":[]}]},{"level":2,"title":"File Downloads","slug":"file-downloads","link":"#file-downloads","children":[{"level":3,"title":"Consuming file downloads","slug":"consuming-file-downloads","link":"#consuming-file-downloads","children":[]},{"level":3,"title":"Database-stored Files","slug":"database-stored-files","link":"#database-stored-files","children":[]},{"level":3,"title":"Other File Storage","slug":"other-file-storage","link":"#other-file-storage","children":[]}]}],"git":{"updatedTime":1678901015000},"filePathRelative":"modeling/model-components/methods.md"}');export{e as data}; diff --git a/assets/methods.html.f2136319.js b/assets/methods.html.f2136319.js new file mode 100644 index 000000000..14bf7c2e2 --- /dev/null +++ b/assets/methods.html.f2136319.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0f867a4b","path":"/stacks/ko/client/methods.html","title":"TypeScript Method Objects","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Base Members","slug":"base-members","link":"#base-members","children":[]},{"level":2,"title":"ListResult Base Members","slug":"listresult-t-base-members","link":"#listresult-t-base-members","children":[]},{"level":2,"title":"Method-specific Members","slug":"method-specific-members","link":"#method-specific-members","children":[]}],"git":{"updatedTime":1654889604000},"filePathRelative":"stacks/ko/client/methods.md"}');export{e as data}; diff --git a/assets/model-config.html.82e5d884.js b/assets/model-config.html.82e5d884.js new file mode 100644 index 000000000..88418d0e0 --- /dev/null +++ b/assets/model-config.html.82e5d884.js @@ -0,0 +1 @@ +const i=JSON.parse('{"key":"v-275d16cf","path":"/stacks/ko/client/model-config.html","title":"ViewModel Configuration","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Hierarchy","slug":"hierarchy","link":"#hierarchy","children":[{"level":3,"title":"Root Configuration","slug":"root-configuration","link":"#root-configuration","children":[]},{"level":3,"title":"Root ViewModel/ListViewModel Configuration","slug":"root-viewmodel-listviewmodel-configuration","link":"#root-viewmodel-listviewmodel-configuration","children":[]},{"level":3,"title":"Class Configuration","slug":"class-configuration","link":"#class-configuration","children":[]},{"level":3,"title":"Instance Configuration","slug":"instance-configuration","link":"#instance-configuration","children":[]}]},{"level":2,"title":"Evaluation","slug":"evaluation","link":"#evaluation","children":[]},{"level":2,"title":"Available Properties & Defaults","slug":"available-properties-defaults","link":"#available-properties-defaults","children":[{"level":3,"title":"Root Configuration","slug":"root-configuration-1","link":"#root-configuration-1","children":[]},{"level":3,"title":"App Configuration","slug":"app-configuration","link":"#app-configuration","children":[]},{"level":3,"title":"ViewModelConfiguration","slug":"viewmodelconfiguration","link":"#viewmodelconfiguration","children":[]},{"level":3,"title":"ListViewModelConfiguration","slug":"listviewmodelconfiguration","link":"#listviewmodelconfiguration","children":[]},{"level":3,"title":"ServiceClientConfiguration","slug":"serviceclientconfiguration","link":"#serviceclientconfiguration","children":[]}]}],"git":{"updatedTime":1654889604000},"filePathRelative":"stacks/ko/client/model-config.md"}');export{i as data}; diff --git a/assets/model-config.html.e7f5edd8.js b/assets/model-config.html.e7f5edd8.js new file mode 100644 index 000000000..a3a965754 --- /dev/null +++ b/assets/model-config.html.e7f5edd8.js @@ -0,0 +1,8 @@ +import{_ as r,y as c,z as d,Q as t,X as e,B as o,$ as n,a5 as s,P as l}from"./framework.fe9a73df.js";const h={},u=s('

    ViewModel Configuration

    A crucial part of the generated TypeScript ViewModels that Coalesce creates for you is the hierarchical configuration system that allows coarse-grained or fine-grained control over their behaviors.

    Hierarchy

    The configuration system has four levels where configuration can be performed, structured as follows:

    Root Configuration

    ',5),f=e("code",null,"app",-1),p=e("h3",{id:"root-viewmodel-listviewmodel-configuration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#root-viewmodel-listviewmodel-configuration","aria-hidden":"true"},"#"),o(" Root ViewModel/ListViewModel Configuration")],-1),g=e("p",null,[o("Additional root configuration objects exist, one for each class kind. These configuration objects govern behavior that applies to only objects of these types. Root configuration "),e("em",null,"can"),o(" be overridden using these objects, although the practicality of doing so is dubious.")],-1),m=e("h3",{id:"class-configuration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#class-configuration","aria-hidden":"true"},"#"),o(" Class Configuration")],-1),v=e("p",null,[o("Each class kind has a static property named "),e("code",null,"coalesceConfig"),o(" that controls behavior for all instances of that class.")],-1),b=e("h3",{id:"instance-configuration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#instance-configuration","aria-hidden":"true"},"#"),o(" Instance Configuration")],-1),w=s('

    Each instance of these classes also has a coalesceConfig property that controls behaviors for that instance only.

    Evaluation

    All configuration properties are Knockout ComputedObservable<T> objects. These observables behave like any other observable - call them with no parameter to obtain the value, call with a parameter to set their value.

    Whenever a configuration property is read from, it first checks its own configuration object for the value of that property. If the explicit value for that configuration object is null, the parent's configuration will be checked for a value. This continues until either a value is found or the root configuration object is reached.

    When a configuration property is given a value, that value is established on that configuration object only. Any dependent configuration objects will not be modified, and if those dependent configuration objects already have a value for that property, their existing value will be used unless that value is later set to null.

    To obtain the raw value for a specific configuration property, call the raw() method on the observable: model.coalesceConfig.autoSaveEnabled.raw().

    Available Properties & Defaults

    The following configuration properties are available. Their default values are also listed. Note that all configuration properties are observables, but for simplicity the documentation below lists the underlying type.

    Root Configuration

    These properties on Coalesce.GlobalConfiguration are available to both ViewModelConfiguration, ListViewModelConfiguration, and ServiceClientConfiguration.

    ',10),_=e("p",null,"The relative url where the API may be found.",-1),y=e("p",null,"The relative url where the admin views may be found.",-1),C=e("p",null,[o("Whether or not the callback specified for "),e("code",null,"onFailure"),o(" will be called or not.")],-1),M=e("p",null,"A callback to be called when a failure response is received from the server.",-1),V=e("p",null,"A callback to be called when an AJAX request begins.",-1),T=e("p",null,"A callback to be called when an AJAX request completes.",-1),S=e("h3",{id:"app-configuration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#app-configuration","aria-hidden":"true"},"#"),o(" App Configuration")],-1),k=e("p",null,[o("These properties on "),e("code",null,"Coalesce.GlobalConfiguration.app"),o(" are not hierarchical - they govern the entire Coalesce application:")],-1),x=e("h3",{id:"viewmodelconfiguration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#viewmodelconfiguration","aria-hidden":"true"},"#"),o(" ViewModelConfiguration")],-1),A=e("p",null,[o("Time to wait after a change is seen before auto-saving (if "),e("code",null,"autoSaveEnabled"),o(" is true). Acts as a debouncing timer for multiple simultaneous changes.")],-1),N=e("p",null,"An array of property names that, if set, will determine which fields will be sent to the server when saving. Only those values that are actually sent to the server will be mapped to the underlying entity.",-1),D=e("p",null,"This can improves the handling of concurrent changes being made by multiple users against different fields of the same entity. Specifically, if one page is designed to edit fields A and B, and another page is designed for editing fields C and D, you can configure this setting appropriately on each page to only save the corresponding fields.",-1),L={class:"custom-container warning"},j=e("p",{class:"custom-container-title"},"WARNING",-1),B=e("p",null,[o("Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the "),e("code",null,"x-www-form-urlencoded"),o(" body that is sent to the server.")],-1),E=e("p",null,[o("Determines whether changes to a model will be automatically saved after "),e("code",null,"saveTimeoutMs"),o(" milliseconds have elapsed.")],-1),G=e("p",null,"Determines whether or not changes to many-to-many collection properties will automatically trigger a save call to the server or not.",-1),F=e("p",null,[o("Whether to invoke "),e("code",null,"onStartBusy"),o(" and "),e("code",null,"onFinishBusy"),o(" during saves.")],-1),O=e("p",null,[o("Whether or not to reload the ViewModel with the state of the object received from the server after a call to "),e("code",null,".save()"),o(".")],-1),W=e("p",null,"Whether or not to validate the model after loading it from a DTO from the server. Disabling this can improve performance in some cases.",-1),R=e("p",null,[o("Whether or not validation on a ViewModel should be setup in its constructor, or if validation must be set up manually by calling "),e("code",null,"viewModel.setupValidation()"),o(". Turning this off can improve performance in read-only scenarios.")],-1),I=e("p",null,"An optional callback to be called when an object is loaded from a response from the server. Callback will be called after all properties on the ViewModel have been set from the server response.",-1),P=e("p",null,[o("The dataSource (either an instance or a type) that will be used as the initial dataSource when a new object of this type is created. Not valid for global configuration; recommended to be used on class-level configuration. E.g. "),e("code",null,"ViewModels.MyModel.coalesceConfig.initialDataSource(MyModel.dataSources.MyDataSource);")],-1),U=e("h3",{id:"listviewmodelconfiguration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#listviewmodelconfiguration","aria-hidden":"true"},"#"),o(" ListViewModelConfiguration")],-1),q=e("p",null,"No special configuration is currently available for ListViewModels.",-1),X=e("h3",{id:"serviceclientconfiguration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#serviceclientconfiguration","aria-hidden":"true"},"#"),o(" ServiceClientConfiguration")],-1),J=e("p",null,"No special configuration is currently available for ServiceClients.",-1);function K($,z){const a=l("Prop"),i=l("RouterLink");return c(),d("div",null,[u,t(a,{def:`Coalesce.GlobalConfiguration: ModelConfiguration +Coalesce.GlobalConfiguration.app: AppConfiguration`,lang:"ts",id:"code-root-config"}),e("p",null,[o("The root configuration contains all configuration properties which apply to class category ("),t(i,{to:"/stacks/ko/client/view-model.html"},{default:n(()=>[o("TypeScript ViewModels")]),_:1}),o(", "),t(i,{to:"/stacks/ko/client/list-view-model.html"},{default:n(()=>[o("TypeScript ListViewModels")]),_:1}),o(", and "),t(i,{to:"/modeling/model-types/services.html"},{default:n(()=>[o("Services")]),_:1}),o("). The "),f,o(" property contains global app configuration that exists independent of any models. Then, for each class kind, the following are available:")]),p,t(a,{def:`Coalesce.GlobalConfiguration.viewModel: ViewModelConfiguration +Coalesce.GlobalConfiguration.listViewModel: ListViewModelConfiguration, BaseViewModel> +Coalesce.GlobalConfiguration.serviceClient: ServiceClientConfiguration`,lang:"ts",id:"code-global-config"}),g,m,t(a,{def:`ViewModels.ClassName.coalesceConfig: ViewModelConfiguration +ListViewModels.ClassNameList.coalesceConfig: ListViewModelConfiguration +Services.ServiceNameClient.coalesceConfig: ServiceClientConfiguration`,lang:"ts",id:"code-class-config"}),v,b,t(a,{def:`instance.coalesceConfig: ViewModelConfiguration +listInstance.coalesceConfig: ListViewModelConfiguration +serviceInstance.coalesceConfig: ServiceClientConfiguration`,lang:"ts",id:"code-instance-config"}),w,t(a,{def:"baseApiUrl: string = '/api'",lang:"ts"}),_,t(a,{def:"baseViewUrl: string = ''",lang:"ts"}),y,t(a,{def:"showFailureAlerts: boolean = true",lang:"ts"}),C,t(a,{def:"onFailure: (obj, message) => alert(message)",lang:"ts"}),M,t(a,{def:"onStartBusy: obj => Coalesce.Utilities.showBusy()",lang:"ts"}),V,t(a,{def:"onFinishBusy: obj => Coalesce.Utilities.hideBusy()",lang:"ts"}),T,S,k,t(a,{def:"select2Theme: string | null = null",lang:"ts"}),e("p",null,[o("The theme parameter to select2's constructor when called by Coalesce's select2 "),t(i,{to:"/stacks/ko/client/bindings.html"},{default:n(()=>[o("Knockout Bindings")]),_:1}),o(".")]),x,t(a,{def:"saveTimeoutMs: number = 500",lang:"ts"}),A,t(a,{def:"saveIncludedFields: string[] | null = null",lang:"ts"}),N,D,e("p",null,[o("Due to design limitations, this cannot be determined dynamically like it can with "),t(i,{to:"/stacks/vue/layers/viewmodels.html"},{default:n(()=>[o("Vue's $saveMode property")]),_:1})]),e("div",L,[j,B,e("p",null,[o("The "),t(i,{to:"/stacks/agnostic/dtos.html"},{default:n(()=>[o("Generated C# DTOs")]),_:1}),o(" implement the necessary logic for this; however, any "),t(i,{to:"/modeling/model-types/dtos.html"},{default:n(()=>[o("Custom DTOs")]),_:1}),o(" must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the "),t(i,{to:"/stacks/agnostic/dtos.html"},{default:n(()=>[o("Generated C# DTOs")]),_:1}),o(", or do not use surgical saves with Custom DTOs.")])]),t(a,{def:"autoSaveEnabled: boolean = true",lang:"ts"}),E,t(a,{def:"autoSaveCollectionsEnabled: boolean = true",lang:"ts"}),G,t(a,{def:"showBusyWhenSaving: boolean = false",lang:"ts"}),F,t(a,{def:"loadResponseFromSaves: boolean = true",lang:"ts"}),O,t(a,{def:"validateOnLoadFromDto: boolean = true",lang:"ts"}),W,t(a,{def:"setupValidationAutomatically: boolean = true",lang:"ts"}),R,t(a,{def:"onLoadFromDto: null | ((object: T) => void) = null",lang:"ts"}),I,t(a,{def:"initialDataSource: null | DataSource | (new () => DataSource) = null",lang:"ts"}),P,U,q,X,J])}const Q=r(h,[["render",K],["__file","model-config.html.vue"]]);export{Q as default}; diff --git a/assets/models.html.598e23b6.js b/assets/models.html.598e23b6.js new file mode 100644 index 000000000..e52cfaf12 --- /dev/null +++ b/assets/models.html.598e23b6.js @@ -0,0 +1,83 @@ +import{_ as D,y as u,z as y,W as d,X as e,B as s,Q as n,$ as o,a5 as c,P as i}from"./framework.fe9a73df.js";const m={},h=e("h1",{id:"vue-model-layer",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#vue-model-layer","aria-hidden":"true"},"#"),s(" Vue Model Layer")],-1),v=e("code",null,"models.g.ts",-1),b=e("code",null,"$metadata",-1),f=e("p",null,[s("The model layer also includes a TypeScript class for each type that can be used to easily instantiate a valid implementation of its corresponding interface. However, it is not necessary for the classes to be used, and all parts of Coalesce that interact with the model layer don't perform any "),e("code",null,"instanceof"),s(" checks against models - the "),e("code",null,"$metadata"),s(" property is used to determine type identity.")],-1),C={class:"table-of-contents"},_=e("h2",{id:"concepts",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#concepts","aria-hidden":"true"},"#"),s(" Concepts")],-1),g={href:"https://github.com/IntelliTect/Coalesce/blob/dev/src/coalesce-vue/src/model.ts",target:"_blank",rel:"noopener noreferrer"},w=e("h3",{id:"model",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#model","aria-hidden":"true"},"#"),s(" Model")],-1),E=e("code",null,"$metadata",-1),A=e("h3",{id:"datasource",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#datasource","aria-hidden":"true"},"#"),s(" DataSource")],-1),T=e("code",null,"$metadata",-1),F=c(`

    Data sources are generated as concrete classes in a namespace named DataSources that is nested inside a namespace named after their parent model type. For example:

    import { Person } from '@/models.g'
    +
    +const dataSource = new Person.DataSources.NamesStartingWith;
    +dataSource.startsWith = "A";
    +// Provide the dataSource to an API Client or a ViewModel...
    +

    Model Functions

    The following functions exported from coalesce-vue can be used with your models:

    `,4),k=e("code",null,"key",-1),S=e("code",null,"obj",-1),x=e("code",null,"queryKey",-1),M={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},z=c("

    The query string will be updated using either router.push or router.replace depending on the value of parameter mode.

    If the query string contains a value when this is called, the object will be updated with that value immediately.

    If the object being bound to has $metadata, information from that metadata will be used to serialize and parse values to and from the query string. Otherwise, String(value) will be used to serialize the value, and the parse parameter (if provided) will be used to parse the value from the query string.

    ",3),P=e("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[e("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[e("code",null,[e("span",{class:"line"},[e("span",{style:{color:"#C586C0"}},"import"),e("span",{style:{color:"#D4D4D4"}}," { "),e("span",{style:{color:"#9CDCFE"}},"bindToQueryString"),e("span",{style:{color:"#D4D4D4"}}," } "),e("span",{style:{color:"#C586C0"}},"from"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#CE9178"}},"'coalesce-vue'"),e("span",{style:{color:"#D4D4D4"}},";")]),s(` +`),e("span",{class:"line"}),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#6A9955"}},"// In the 'created' Vue lifecycle hook on a component:")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#DCDCAA"}},"created"),e("span",{style:{color:"#D4D4D4"}},"() {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#6A9955"}},"// Bind pagination information to the query string:")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"bindToQueryString"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"listViewModel"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"$params"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'pageSize'"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'pageSize'"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#9CDCFE"}},"v"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#569CD6"}},"=>"),e("span",{style:{color:"#D4D4D4"}}," +"),e("span",{style:{color:"#9CDCFE"}},"v"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"}),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#6A9955"}},"// Assuming the component has an 'activeTab' data member:")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"bindToQueryString"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'activeTab'"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}},"}")]),s(` +`),e("span",{class:"line"})])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),I=e("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[e("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[e("code",null,[e("span",{class:"line"},[e("span",{style:{color:"#C586C0"}},"import"),e("span",{style:{color:"#D4D4D4"}}," { "),e("span",{style:{color:"#9CDCFE"}},"useBindToQueryString"),e("span",{style:{color:"#D4D4D4"}}," } "),e("span",{style:{color:"#C586C0"}},"from"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#CE9178"}},"'coalesce-vue'"),e("span",{style:{color:"#D4D4D4"}},";")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#DCDCAA"}},"setup"),e("span",{style:{color:"#D4D4D4"}},"() {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#6A9955"}},"// Bind pagination information to the query string:")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#569CD6"}},"const"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#4FC1FF"}},"list"),e("span",{style:{color:"#D4D4D4"}}," = "),e("span",{style:{color:"#569CD6"}},"new"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"PersonListViewModel"),e("span",{style:{color:"#D4D4D4"}},"();")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"useBindToQueryString"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#9CDCFE"}},"list"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"$params"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'pageSize'"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'pageSize'"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#9CDCFE"}},"v"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#569CD6"}},"=>"),e("span",{style:{color:"#D4D4D4"}}," +"),e("span",{style:{color:"#9CDCFE"}},"v"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"}),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#569CD6"}},"const"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#4FC1FF"}},"activeTab"),e("span",{style:{color:"#D4D4D4"}}," = "),e("span",{style:{color:"#DCDCAA"}},"ref"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#CE9178"}},'"1"'),e("span",{style:{color:"#D4D4D4"}},")")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"useBindToQueryString"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#9CDCFE"}},"activeTab"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'value'"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#CE9178"}},"'activeTab'"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}},"}")]),s(` +`),e("span",{class:"line"})])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),j=e("p",null,[s("When "),e("code",null,"model"),s(" is created (i.e. its primary key becomes non-null), replace the current URL with one that includes uses primary key for the route parameter named by "),e("code",null,"routeParamName"),s(".")],-1),O=e("p",null,[s("The query string will not be kept when the route is changed unless "),e("code",null,"true"),s(" is given to "),e("code",null,"keepQuery"),s(".")],-1),V=e("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[e("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[e("code",null,[e("span",{class:"line"},[e("span",{style:{color:"#C586C0"}},"import"),e("span",{style:{color:"#D4D4D4"}}," { "),e("span",{style:{color:"#9CDCFE"}},"bindKeyToRouteOnCreate"),e("span",{style:{color:"#D4D4D4"}}," } "),e("span",{style:{color:"#C586C0"}},"from"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#CE9178"}},"'coalesce-vue'"),e("span",{style:{color:"#D4D4D4"}},";")]),s(` +`),e("span",{class:"line"}),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#6A9955"}},"// In the 'created' Vue lifecycle hook on a component:")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#DCDCAA"}},"created"),e("span",{style:{color:"#D4D4D4"}},"() {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#C586C0"}},"if"),e("span",{style:{color:"#D4D4D4"}}," ("),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"id"),e("span",{style:{color:"#D4D4D4"}},") {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"viewModel"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#DCDCAA"}},"$load"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"id"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," } "),e("span",{style:{color:"#C586C0"}},"else"),e("span",{style:{color:"#D4D4D4"}}," {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"bindKeyToRouteOnCreate"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},", "),e("span",{style:{color:"#569CD6"}},"this"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"viewModel"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," }")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}},"}")]),s(` +`),e("span",{class:"line"})])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),q=e("div",{class:"language-typescript line-numbers-mode","data-ext":"ts"},[e("pre",{class:"shiki",style:{"background-color":"#1E1E1E"}},[e("code",null,[e("span",{class:"line"},[e("span",{style:{color:"#C586C0"}},"import"),e("span",{style:{color:"#D4D4D4"}}," { "),e("span",{style:{color:"#9CDCFE"}},"useBindKeyToRouteOnCreate"),e("span",{style:{color:"#D4D4D4"}}," } "),e("span",{style:{color:"#C586C0"}},"from"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#CE9178"}},"'coalesce-vue'"),e("span",{style:{color:"#D4D4D4"}},";")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#DCDCAA"}},"setup"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#9CDCFE"}},"props"),e("span",{style:{color:"#D4D4D4"}},") {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#569CD6"}},"const"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#4FC1FF"}},"viewModel"),e("span",{style:{color:"#D4D4D4"}}," = "),e("span",{style:{color:"#569CD6"}},"new"),e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"PersonViewModel"),e("span",{style:{color:"#D4D4D4"}},"();")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#C586C0"}},"if"),e("span",{style:{color:"#D4D4D4"}}," ("),e("span",{style:{color:"#9CDCFE"}},"props"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"id"),e("span",{style:{color:"#D4D4D4"}},") {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#9CDCFE"}},"viewModel"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#DCDCAA"}},"$load"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#9CDCFE"}},"props"),e("span",{style:{color:"#D4D4D4"}},"."),e("span",{style:{color:"#9CDCFE"}},"id"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," } "),e("span",{style:{color:"#C586C0"}},"else"),e("span",{style:{color:"#D4D4D4"}}," {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," "),e("span",{style:{color:"#DCDCAA"}},"useBindToQueryString"),e("span",{style:{color:"#D4D4D4"}},"("),e("span",{style:{color:"#9CDCFE"}},"viewModel"),e("span",{style:{color:"#D4D4D4"}},");")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}}," }")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#D4D4D4"}},"}")]),s(` +`),e("span",{class:"line"})])]),e("div",{class:"line-numbers","aria-hidden":"true"},[e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"}),e("div",{class:"line-number"})])],-1),B={class:"custom-container tip"},N=e("p",{class:"custom-container-title"},"Note",-1),R={href:"https://developer.mozilla.org/en-US/docs/Web/API/History_API",target:"_blank",rel:"noopener noreferrer"},Q={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},L=e("code",null,"",-1),K=e("h2",{id:"advanced-model-functions",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#advanced-model-functions","aria-hidden":"true"},"#"),s(" Advanced Model Functions")],-1),W=e("p",null,[s("The following functions exported from "),e("code",null,"coalesce-vue"),s(" can be used with your models.")],-1),$={class:"custom-container tip"},Z=e("p",{class:"custom-container-title"},"Note",-1),J=e("p",null,"While you're absolutely free to use them in your own code and can rely on their interface and behavior to remain consistent, you will find that you seldom need to use them directly - that's why we've split them into their own section here in the documentation.",-1),U=c("

    Given any JavaScript value value, convert it into a valid implementation of the value or type described by metadata.

    For metadata describing a primitive or primitive-like value, the input will be parsed into a valid implementation of the correct JavaScript type. For example, for metadata that describes a boolean, a string "true" will return a boolean true, and ISO 8601 date strings will result in a JavaScript Date object.

    For metadata describing a type, the input object will be mutated into a valid implementation of the appropriate model interface. Missing properties will be set to null, and any descendent properties of the provided object will be recursively processed with convertToModel.

    If any values are encountered that are fundamentally incompatible with the requested type described by the metadata, an error will be thrown.

    ",4),H=e("p",null,[s("Performs the same operations as "),e("code",null,"convertToModel"),s(", except that any objects encountered will not be mutated - instead, a new object or array will always be created.")],-1),G=e("p",null,"Maps the input to a representation suitable for JSON serialization.",-1),X=e("p",null,[s("Will not serialize child objects or collections whose metadata includes "),e("code",null,"dontSerialize"),s(". Will only recurse to a maximum depth of 3.")],-1),Y=e("p",null,[e("a",{id:"VueModelDisplayFunctions"})],-1),ee=e("p",null,[s("Returns a string representing the "),e("code",null,"model"),s(" suitable for display in a user interface.")],-1),se=e("code",null,"displayProp",-1),ne=e("code",null,"displayProp",-1),oe=e("p",null,[s("See "),e("a",{href:"#displayoptions"},"DisplayOptions"),s(" for available options.")],-1),ae=e("p",null,"Returns a string representing the specified property of the given object suitable for display in a user interface.",-1),le=e("p",null,[s("The property can either be a string, representing one of the model's properties, or the actual "),e("code",null,"Property"),s(" metadata object of the property.")],-1),te=e("p",null,[s("See "),e("a",{href:"#displayoptions"},"DisplayOptions"),s(" for available options.")],-1),re=c(`

    Returns a string representing the given value (described by the given metadata).

    See DisplayOptions for available options.

    DisplayOptions

    The following options are available to functions in coalesce-vue that render a value or object for display:

    export interface DisplayOptions {
    +  /** Date format options. One of:
    +   * - A UTS#35 date format string (https://date-fns.org/docs/format)
    +   * - An object with options for https://date-fns.org/docs/format or https://github.com/marnusw/date-fns-tz#format, including a string \`format\` for the format itself. If a \`timeZone\` option is provided per https://github.com/marnusw/date-fns-tz#format, the date being formatted will be converted to that timezone.
    +   * - An object with options for https://date-fns.org/docs/formatDistance */
    +  format?:
    +    | string
    +    | ({
    +        /** A UTS#35 date format string (https://date-fns.org/docs/format) */
    +        format: string;
    +      } & Parameters<typeof format>[2])
    +    | {
    +        /** Format date with https://date-fns.org/docs/formatDistanceToNow */
    +        distance: true;
    +        /** Append/prepend \`'in'\` or \`'ago'\` if date is after/before now. Default \`true\`. */
    +        addSuffix?: boolean;
    +        /** Include detail smaller than one minute. Default \`false\`. */
    +        includeSeconds?: boolean;
    +      };
    +
    +  collection?: {
    +    /** The maximum number of items to display individually.
    +     * When there are more than this number of items, the count of items will be displayed instead.
    +     * Default \`5\`.
    +     * */
    +    enumeratedItemsMax?: number;
    +
    +    /** The separator to place between enumerated items. Default \`', '\` */
    +    enumeratedItemsSeparator?: string;
    +  };
    +}
    +
    `,5),ie={class:"custom-container tip"},ce=e("p",{class:"custom-container-title"},"Note",-1),pe=e("code",null,"formatDistanceToNow",-1),de={href:"https://vuejs.org/api/built-in-special-attributes.html#key",target:"_blank",rel:"noopener noreferrer"},De=e("h2",{id:"time-zones",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#time-zones","aria-hidden":"true"},"#"),s(" Time Zones")],-1),ue=e("p",null,[s("In Coalesce Vue, all "),e("code",null,"DateTimeOffset"),s("-based properties, for both inputs and display-only contexts, are by default formatted into the user's computer's system time zone. This is largely just a consequence of how the JavaScript Date type works. However, this behavior can be overridden by configuring a global default timezone, or by providing a time zone name to individual usages.")],-1),ye=e("p",null,[s("Fields with a type of "),e("code",null,"DateTime"),s(" are agnostic to time zone and UTC offset and so are not subject to any of the following rules.")],-1),me={href:"https://en.wikipedia.org/wiki/List_of_tz_database_time_zones",target:"_blank",rel:"noopener noreferrer"},he=e("code",null,'"America/Los_Angeles"',-1),ve=e("p",null,"The time zone provided here is used in the following ways:",-1),be=e("code",null,"DisplayOptions.format.timeZone",-1),fe=e("a",{href:"#member-modeldisplay"},"modelDisplay",-1),Ce=e("a",{href:"#member-propdisplay"},"propDisplay",-1),_e=e("a",{href:"#member-valuedisplay"},"valueDisplay",-1),ge=e("code",null,"timeZone",-1),we=e("li",null,"It will be used when serializing DateTimeOffset fields into JSON DTOs, representing the ISO 8601 date string in the specified time zone rather than in the user's computer's system time zone.",-1),Ee=e("p",null,[s("Returns the current configured default time zone. Default is "),e("code",null,"null"),s(", falling back on the user's computer's system time zone.")],-1);function Ae(Te,Fe){const a=i("RouterLink"),t=i("router-link"),r=i("ExternalLinkIcon"),l=i("Prop"),p=i("CodeTabs");return u(),y("div",null,[h,d(" MARKER:summary "),e("p",null,[s("The model layer, generated as "),v,s(", contains a set of TypeScript interfaces that represent each client-exposed type in your data model. Each interface contains all the "),n(a,{to:"/modeling/model-components/properties.html"},{default:o(()=>[s("Properties")]),_:1}),s(" of that type, as well as a "),b,s(" property that references the "),n(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[s("metadata")]),_:1}),s(" object for that type. Enums and "),n(a,{to:"/modeling/model-components/data-sources.html"},{default:o(()=>[s("Data Sources")]),_:1}),s(" are also represented in the model layer.")]),d(" MARKER:summary-end "),f,e("nav",C,[e("ul",null,[e("li",null,[n(t,{to:"#concepts"},{default:o(()=>[s("Concepts")]),_:1}),e("ul",null,[e("li",null,[n(t,{to:"#model"},{default:o(()=>[s("Model")]),_:1})]),e("li",null,[n(t,{to:"#datasource"},{default:o(()=>[s("DataSource")]),_:1})])])]),e("li",null,[n(t,{to:"#model-functions"},{default:o(()=>[s("Model Functions")]),_:1})]),e("li",null,[n(t,{to:"#advanced-model-functions"},{default:o(()=>[s("Advanced Model Functions")]),_:1})]),e("li",null,[n(t,{to:"#displayoptions"},{default:o(()=>[s("DisplayOptions")]),_:1})]),e("li",null,[n(t,{to:"#time-zones"},{default:o(()=>[s("Time Zones")]),_:1})])])]),_,e("p",null,[s("The model layer is fairly simple - the only main concept it introduces on top of the "),n(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[s("Metadata Layer")]),_:1}),s(" is the notion of interfaces and enums that mirror the C# types in your data model. As with the "),n(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[s("Metadata Layer")]),_:1}),s(", the "),e("a",g,[s("source code of coalesce-vue"),n(r)]),s(" is a great documentation supplement to this page.")]),w,e("p",null,[s("An interface describing an instance of a class type from your application's data model. All Model interfaces contain members for all the "),n(a,{to:"/modeling/model-components/properties.html"},{default:o(()=>[s("Properties")]),_:1}),s(" of that type, as well as a "),E,s(" property that references the metadata object for that type.")]),A,e("p",null,[s("A class-based representation of a "),n(a,{to:"/modeling/model-components/data-sources.html"},{default:o(()=>[s("Data Source")]),_:1}),s(" containing properties for any of the "),n(a,{to:"/modeling/model-components/data-sources.html#custom-parameters"},{default:o(()=>[s("Custom Parameters")]),_:1}),s(" of the data source, as well as a "),T,s(" property that references the metadata object for the data source.")]),F,n(l,{def:`// Vue Options API +bindToQueryString(vue: Vue, obj: {}, key: string, queryKey: string = key, parse?: (v: any) => any, mode: 'push' | 'replace' = 'replace') +  +// Vue Composition API +useBindToQueryString(obj: {}, key: string, queryKey: string = key, parse?: (v: any) => any, mode: 'push' | 'replace' = 'replace')`,lang:"ts",idPrefix:"member-bindToQuery"}),e("p",null,[s("Binds property "),k,s(" of "),S,s(" to query string parameter "),x,s(". When the object's value changes, the query string will be updated using "),e("a",M,[s("vue-router"),n(r)]),s(". When the query string changes, the object's value will be updated.")]),z,n(p,{name:"vue"},{options:o(()=>[P]),setup:o(()=>[I]),_:1}),n(l,{def:`// Vue Options API +bindKeyToRouteOnCreate(vue: Vue, model: Model, routeParamName: string = 'id', keepQuery: boolean = false) +  +// Vue Composition API +useBindKeyToRouteOnCreate(model: Model, routeParamName: string = 'id', keepQuery: boolean = false)`,lang:"ts",idPrefix:"member-bindKey"}),j,O,n(p,{name:"vue"},{options:o(()=>[V]),setup:o(()=>[q]),_:1}),e("div",B,[N,e("p",null,[s("The route will be replaced directly via the "),e("a",R,[s("HTML5 History API"),n(r)]),s(" such that "),e("a",Q,[s("vue-router"),n(r)]),s(" will not observe the change as an actual route change, preventing the current view from being recreated if a path-based key is being used on the application's "),L,s(" component.")])]),K,W,e("div",$,[Z,e("p",null,[s("These functions are used to implement the "),n(a,{to:"/stacks/vue/overview.html"},{default:o(()=>[s("higher-order layers")]),_:1}),s(" in the Vue stack.")]),J]),n(l,{def:"convertToModel(value: any, metadata: Value | ClassType): any",lang:"ts"}),U,n(l,{def:"mapToModel(value: any, metadata: Value | ClassType): any",lang:"ts"}),H,n(l,{def:"mapToDto(value: any, metadata: Value | ClassType): any",lang:"ts"}),G,X,Y,n(l,{def:"modelDisplay(model: Model, options?: DisplayOptions): string",lang:"ts"}),ee,e("p",null,[s("Uses the "),se,s(" defined on the object's metadata. If no "),ne,s(" is defined, the object will be displayed as JSON. The display prop on a model can be defined in C# with "),n(a,{to:"/modeling/model-components/attributes/list-text.html"},{default:o(()=>[s("[ListText]")]),_:1}),s(".")]),oe,n(l,{def:"propDisplay(model: Model, prop: Property | string, options?: DisplayOptions): string",lang:"ts"}),ae,le,te,n(l,{def:"valueDisplay(value: any, metadata: Value, options?: DisplayOptions): string",lang:"ts"}),re,e("div",ie,[ce,e("p",null,[s("Dates rendered with the "),pe,s(" function into a Vue component will not automatically be updated in realtime. If this is needed, you should use a strategy like using a "),e("a",de,[s("key"),n(r)]),s(" that you periodically update to force a re-render.")])]),De,ue,ye,n(l,{def:"setDefaultTimeZone(timeZoneName: string | null): void",lang:"ts"}),e("p",null,[s("Gets or sets the default time zone used by Coalesce. The time zone should be an "),e("a",me,[s("IANA Time Zone Database"),n(r)]),s(" name, e.g. "),he,s(".")]),ve,e("ul",null,[e("li",null,[s("It will be used as "),be,s(" if no other value was provided for this option. This is used by functions "),fe,s(", "),Ce,s(", and "),_e,s(", as well as the "),n(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:o(()=>[s("c-display")]),_:1}),s(" component.")]),e("li",null,[s("It will be used by "),n(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html"},{default:o(()=>[s("c-datetime-picker")]),_:1}),s(", used to both interpret the user input and display the selected date. This can also be set on individual component usages via the "),ge,s(" prop.")]),we]),n(l,{def:"getDefaultTimeZone(): string | null",lang:"ts"}),Ee])}const Se=D(m,[["render",Ae],["__file","models.html.vue"]]);export{Se as default}; diff --git a/assets/models.html.b9811d9a.js b/assets/models.html.b9811d9a.js new file mode 100644 index 000000000..17f27b614 --- /dev/null +++ b/assets/models.html.b9811d9a.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0cd91dd6","path":"/stacks/vue/layers/models.html","title":"Vue Model Layer","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Concepts","slug":"concepts","link":"#concepts","children":[{"level":3,"title":"Model","slug":"model","link":"#model","children":[]},{"level":3,"title":"DataSource","slug":"datasource","link":"#datasource","children":[]}]},{"level":2,"title":"Model Functions","slug":"model-functions","link":"#model-functions","children":[]},{"level":2,"title":"Advanced Model Functions","slug":"advanced-model-functions","link":"#advanced-model-functions","children":[]},{"level":2,"title":"DisplayOptions","slug":"displayoptions","link":"#displayoptions","children":[]},{"level":2,"title":"Time Zones","slug":"time-zones","link":"#time-zones","children":[]}],"git":{"updatedTime":1677795733000},"filePathRelative":"stacks/vue/layers/models.md"}');export{e as data}; diff --git a/assets/overview.html.36fc7252.js b/assets/overview.html.36fc7252.js new file mode 100644 index 000000000..c00bcd09a --- /dev/null +++ b/assets/overview.html.36fc7252.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-33aa6bf4","path":"/stacks/vue/coalesce-vue-vuetify/overview.html","title":"Vuetify Components","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Setup","slug":"setup","link":"#setup","children":[]},{"level":2,"title":"Display Components","slug":"display-components","link":"#display-components","children":[]},{"level":2,"title":"Input Components","slug":"input-components","link":"#input-components","children":[]},{"level":2,"title":"Admin Components","slug":"admin-components","link":"#admin-components","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"stacks/vue/coalesce-vue-vuetify/overview.md"}');export{e as data}; diff --git a/assets/overview.html.48ae847f.js b/assets/overview.html.48ae847f.js new file mode 100644 index 000000000..c0cababac --- /dev/null +++ b/assets/overview.html.48ae847f.js @@ -0,0 +1 @@ +import{_ as i,y as d,z as r,W as c,X as t,Q as l,B as e,$ as o,P as u}from"./framework.fe9a73df.js";const p={},m=t("h1",{id:"vuetify-components",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#vuetify-components","aria-hidden":"true"},"#"),e(" Vuetify Components")],-1),h={href:"https://www.npmjs.com/package/coalesce-vue-vuetify2",target:"_blank",rel:"noopener noreferrer"},f=t("img",{src:"https://img.shields.io/npm/v/coalesce-vue-vuetify2/latest?color=42b883&label=coalesce-vue-vuetify2%40latest",alt:""},null,-1),v={href:"https://www.npmjs.com/package/coalesce-vue-vuetify3",target:"_blank",rel:"noopener noreferrer"},_=t("img",{src:"https://img.shields.io/npm/v/coalesce-vue-vuetify3/latest?color=42b883&label=coalesce-vue-vuetify3%40latest",alt:""},null,-1),y={href:"https://vuejs.org/",target:"_blank",rel:"noopener noreferrer"},g={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},k={href:"https://www.npmjs.com/package/coalesce-vue-vuetify2",target:"_blank",rel:"noopener noreferrer"},b={href:"https://www.npmjs.com/package/coalesce-vue-vuetify3",target:"_blank",rel:"noopener noreferrer"},w={class:"table-of-contents"},A=t("h2",{id:"setup",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#setup","aria-hidden":"true"},"#"),e(" Setup")],-1),V=t("p",null,"If for whatever reason you find yourself adding Coalesce to an existing project, use the template as a reference for what configuration needs to be added to your project.",-1),x=t("h2",{id:"display-components",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#display-components","aria-hidden":"true"},"#"),e(" Display Components")],-1),C=t("thead",null,[t("tr",null,[t("th",{width:"170px"},"Component"),t("th",null,"Description")])],-1),M={class:"custom-container tip"},j=t("p",{class:"custom-container-title"},"TIP",-1),L=t("code",null,"$items",-1),I=t("code",null," - of ",-1),P=t("h2",{id:"input-components",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#input-components","aria-hidden":"true"},"#"),e(" Input Components")],-1),z=t("thead",null,[t("tr",null,[t("th",{width:"170px"},"Component"),t("th",null,"Description")])],-1),D={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},R=t("td",null,[t("p",null,[e("A dropdown component that allows for selecting values fetched from the generated "),t("code",null,"/list"),e(" API endpoints.")]),t("p",null,"Used both for selecting values for foreign key and navigation properties, and for selecting arbitrary objects or primary keys independent of a parent or owning object.")],-1),T=t("code",null,"v-model",-1),E=t("code",null,"/list",-1),B=t("td",null,[t("p",null,"A dropdown component that will present a list of suggested string values from a custom API endpoint. Allows users to input values that aren't provided by the endpoint."),t("p",null,"Effectively, this is a server-driven autocomplete list.")],-1),N=t("td",null,[t("p",null,"A multi-select input component for collections of non-object values (primarily strings and numbers).")],-1),S=t("code",null,"filters",-1),F=t("code",null,"pageSize",-1),K=t("code",null,"page",-1),$=t("h2",{id:"admin-components",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#admin-components","aria-hidden":"true"},"#"),e(" Admin Components")],-1),G=t("thead",null,[t("tr",null,[t("th",{width:"170px"},"Component"),t("th",null,"Description")])],-1),Q={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},U={href:"https://vuetifyjs.com/en/components/expansion-panels/",target:"_blank",rel:"noopener noreferrer"},W={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"},X={href:"https://router.vuejs.org/",target:"_blank",rel:"noopener noreferrer"};function q(H,J){const n=u("ExternalLinkIcon"),a=u("RouterLink"),s=u("router-link");return d(),r("div",null,[m,c(" MARKER:summary "),t("p",null,[t("a",h,[f,l(n)]),t("a",v,[_,l(n)])]),t("p",null,[e("The "),t("a",y,[e("Vue"),l(n)]),e(" stack for Coalesce provides "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:o(()=>[e("a set of components")]),_:1}),e(" based on "),t("a",g,[e("Vuetify"),l(n)]),e(", packaged up in an NPM package "),t("a",k,[e("coalesce-vue-vuetify2"),l(n)]),e(" or "),t("a",b,[e("coalesce-vue-vuetify3"),l(n)]),e(". These components are driven primarily by the "),l(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("Metadata Layer")]),_:1}),e(", and include both low level input and display components like "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:o(()=>[e("c-input")]),_:1}),e(" and "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:o(()=>[e("c-display")]),_:1}),e(" that are highly reusable in the custom pages you'll build in your application, as well as high-level components like "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html"},{default:o(()=>[e("c-admin-table-page")]),_:1}),e(" and "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html"},{default:o(()=>[e("c-admin-editor-page")]),_:1}),e(" that constitute entire pages.")]),c(" MARKER:summary-end "),t("nav",w,[t("ul",null,[t("li",null,[l(s,{to:"#setup"},{default:o(()=>[e("Setup")]),_:1})]),t("li",null,[l(s,{to:"#display-components"},{default:o(()=>[e("Display Components")]),_:1})]),t("li",null,[l(s,{to:"#input-components"},{default:o(()=>[e("Input Components")]),_:1})]),t("li",null,[l(s,{to:"#admin-components"},{default:o(()=>[e("Admin Components")]),_:1})])])]),A,t("p",null,[e("All Coalesce projects should be started from the template described in "),l(a,{to:"/stacks/vue/getting-started.html"},{default:o(()=>[e("Getting Started with Vue")]),_:1}),e(", and will therefore have all the setup completed for you.")]),V,x,t("table",null,[C,t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:o(()=>[e("c-display")]),_:1})])]),t("td",null,[t("p",null,[e("A general-purpose component for displaying any "),l(a,{to:"/stacks/vue/layers/metadata.html#value"},{default:o(()=>[e("Value")]),_:1}),e(" by rendering the value to a string with the "),l(a,{to:"/stacks/vue/layers/models.html#VueModelDisplayFunctions"},{default:o(()=>[e("display functions from the Models Layer")]),_:1}),e(". For plain string and number "),l(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("values")]),_:1}),e(", usage of this component is largely superfluous. For all other value types including dates, booleans, enums, objects, and collections, it is very handy.")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html"},{default:o(()=>[e("c-loader-status")]),_:1})])]),t("td",null,[t("p",null,[e("A component for displaying progress and error information for one or more "),l(a,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:o(()=>[e("API Callers")]),_:1}),e(".")]),t("div",M,[j,t("p",null,[e("It is highly recommended that all "),l(a,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:o(()=>[e("API Callers")]),_:1}),e(" utilized by your application that don't have any other kind of error handling should be represented by a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html"},{default:o(()=>[e("c-loader-status")]),_:1}),e(" so that users can be aware of any errors that occur.")])])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html"},{default:o(()=>[e("c-list-range-display")]),_:1})])]),t("td",null,[t("p",null,[e("Displays pagination information about the current "),L,e(" of a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(" in the format "),I,e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-table.html"},{default:o(()=>[e("c-table")]),_:1})])]),t("td",null,[t("p",null,[e("A table component for displaying the contents of a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(". Also supports modifying the list's [sort parameters](/modeling/model-components/data-sources.md#standard-parameters) by clicking on column headers. Pairs well with a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html"},{default:o(()=>[e("c-list-pagination")]),_:1}),e(".")])])])]),P,t("table",null,[z,t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:o(()=>[e("c-input")]),_:1})])]),t("td",null,[t("p",null,[e("A general-purpose input component for most "),l(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("Values")]),_:1}),e(". c-input does not have much functionality of its own - instead, it delegates to the right kind of component based on the type of value to which it is bound. This includes both other "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:o(()=>[e("Coalesce Vuetify Components")]),_:1}),e(" as well as direct usages of some "),t("a",D,[e("Vuetify"),l(n)]),e(" components.")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select.html"},{default:o(()=>[e("c-select")]),_:1})])]),R]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html"},{default:o(()=>[e("c-datetime-picker")]),_:1})])]),t("td",null,[t("p",null,[e("A general, all-purpose date/time input component that can be used either with "),l(a,{to:"/stacks/vue/layers/models.html"},{default:o(()=>[e("models")]),_:1}),e(" and "),l(a,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("metadata")]),_:1}),e(" or as a standalone component using only "),T,e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html"},{default:o(()=>[e("c-select-many-to-many")]),_:1})])]),t("td",null,[t("p",null,[e("A multi-select dropdown component that allows for selecting values fetched from the generated "),E,e(" API endpoints for collection navigation properties that were annotated with "),l(a,{to:"/modeling/model-components/attributes/many-to-many.html"},{default:o(()=>[e("[ManyToMany]")]),_:1}),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.html"},{default:o(()=>[e("c-select-string-value")]),_:1})])]),B]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html"},{default:o(()=>[e("c-select-values")]),_:1})])]),N]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html"},{default:o(()=>[e("c-list-filters")]),_:1})])]),t("td",null,[t("p",null,[e("A component that provides an interface for modifying the "),S,e(" prop of a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e("'s "),l(a,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:o(()=>[e("parameters")]),_:1}),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html"},{default:o(()=>[e("c-list-pagination")]),_:1})])]),t("td",null,[t("p",null,[e("A component that provides an interface for modifying the pagination "),l(a,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:o(()=>[e("parameters")]),_:1}),e(" of a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")]),t("p",null,[e("This is a composite of "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:o(()=>[e("c-list-page-size")]),_:1}),e(", "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html"},{default:o(()=>[e("c-list-range-display")]),_:1}),e(", and "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html"},{default:o(()=>[e("c-list-page")]),_:1}),e(", arranged horizontally. It is designed to be used above or below a table (e.g. "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-table.html"},{default:o(()=>[e("c-table")]),_:1}),e(").")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:o(()=>[e("c-list-page-size")]),_:1})])]),t("td",null,[t("p",null,[e("A component that provides an dropdown for modifying the "),F,e(),l(a,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:o(()=>[e("parameter")]),_:1}),e(" prop of a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html"},{default:o(()=>[e("c-list-page")]),_:1})])]),t("td",null,[t("p",null,[e("A component that provides previous/next buttons and a text field for modifying the "),K,e(),l(a,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:o(()=>[e("parameter")]),_:1}),e(" prop of a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")])])])]),$,t("table",null,[G,t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html"},{default:o(()=>[e("c-admin-method")]),_:1})])]),t("td",null,[t("p",null,[e("Provides an interface for invoking a "),l(a,{to:"/modeling/model-components/methods.html"},{default:o(()=>[e("method")]),_:1}),e(" and rendering its result, designed to be use in an admin page.")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html"},{default:o(()=>[e("c-admin-methods")]),_:1})])]),t("td",null,[t("p",null,[e("Renders in a "),t("a",Q,[e("Vuetify"),l(n)]),e(),t("a",U,[e("v-expansion-panels"),l(n)]),e(" a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html"},{default:o(()=>[e("c-admin-method")]),_:1}),e(" for each method on a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ViewModel")]),_:1}),e(" or "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html"},{default:o(()=>[e("c-admin-display")]),_:1})])]),t("td",null,[t("p",null,[e("Behaves the same as "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:o(()=>[e("c-display")]),_:1}),e(", except any collection navigation properties will be rendered as links to an admin list page, and any models will be rendered as a link to an admin item page.")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html"},{default:o(()=>[e("c-admin-editor")]),_:1})])]),t("td",null,[t("p",null,[e("An editor for a single "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ViewModel")]),_:1}),e(" instance. Provides a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:o(()=>[e("c-input")]),_:1}),e(" for each property of the model.")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html"},{default:o(()=>[e("c-admin-editor-page")]),_:1})])]),t("td",null,[t("p",null,[e("A page for a creating/editing single "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ViewModel")]),_:1}),e(" instance. Provides a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html"},{default:o(()=>[e("c-admin-editor")]),_:1}),e(" and a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html"},{default:o(()=>[e("c-admin-methods")]),_:1}),e(" for the instance. Designed to be routed to directly with "),t("a",W,[e("vue-router"),l(n)]),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html"},{default:o(()=>[e("c-admin-table")]),_:1})])]),t("td",null,[t("p",null,[e("An full-featured table for a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(", including a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html"},{default:o(()=>[e("c-admin-table-toolbar")]),_:1}),e(", "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-table.html"},{default:o(()=>[e("c-table")]),_:1}),e(", and "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html"},{default:o(()=>[e("c-list-pagination")]),_:1}),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html"},{default:o(()=>[e("c-admin-table-toolbar")]),_:1})])]),t("td",null,[t("p",null,[e("A full-featured toolbar for a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(' designed to be used on an admin page, including "Create" and "Reload" buttons, a '),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html"},{default:o(()=>[e("c-list-range-display")]),_:1}),e(", a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html"},{default:o(()=>[e("c-list-page")]),_:1}),e(", a search field, "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html"},{default:o(()=>[e("c-list-filters")]),_:1}),e(", and a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html"},{default:o(()=>[e("c-list-page-size")]),_:1}),e(".")])])]),t("tr",null,[t("td",null,[t("p",null,[l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html"},{default:o(()=>[e("c-admin-table-page")]),_:1})])]),t("td",null,[t("p",null,[e("A full-featured page for interacting with a "),l(a,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ListViewModel")]),_:1}),e(". Provides a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html"},{default:o(()=>[e("c-admin-table")]),_:1}),e(" and a "),l(a,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html"},{default:o(()=>[e("c-admin-methods")]),_:1}),e(" for the list. Designed to be routed to directly with "),t("a",X,[e("vue-router"),l(n)]),e(".")])])])])])}const Y=i(p,[["render",q],["__file","overview.html.vue"]]);export{Y as default}; diff --git a/assets/overview.html.9101164c.js b/assets/overview.html.9101164c.js new file mode 100644 index 000000000..07aa3c42d --- /dev/null +++ b/assets/overview.html.9101164c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-930615c0","path":"/stacks/ko/overview.html","title":"Knockout Overview","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":2,"title":"Generated Code","slug":"generated-code","link":"#generated-code","children":[{"level":3,"title":"TypeScript","slug":"typescript","link":"#typescript","children":[]},{"level":3,"title":"View Controllers","slug":"view-controllers","link":"#view-controllers","children":[]},{"level":3,"title":"Admin Views","slug":"admin-views","link":"#admin-views","children":[]}]}],"git":{"updatedTime":1663268139000},"filePathRelative":"stacks/ko/overview.md"}');export{e as data}; diff --git a/assets/overview.html.9d82da2f.js b/assets/overview.html.9d82da2f.js new file mode 100644 index 000000000..3f29cb8e2 --- /dev/null +++ b/assets/overview.html.9d82da2f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-4966afe8","path":"/stacks/vue/overview.html","title":"Vue Overview","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Getting Started","slug":"getting-started","link":"#getting-started","children":[]},{"level":2,"title":"TypeScript Layers","slug":"typescript-layers","link":"#typescript-layers","children":[{"level":3,"title":"Metadata Layer","slug":"metadata-layer","link":"#metadata-layer","children":[]},{"level":3,"title":"Model Layer","slug":"model-layer","link":"#model-layer","children":[]},{"level":3,"title":"API Client Layer","slug":"api-client-layer","link":"#api-client-layer","children":[]},{"level":3,"title":"ViewModel Layer","slug":"viewmodel-layer","link":"#viewmodel-layer","children":[]}]},{"level":2,"title":"Vue Components","slug":"vue-components","link":"#vue-components","children":[]},{"level":2,"title":"Admin Views","slug":"admin-views","link":"#admin-views","children":[]}],"git":{"updatedTime":1678412142000},"filePathRelative":"stacks/vue/overview.md"}');export{e as data}; diff --git a/assets/overview.html.f23b8be3.js b/assets/overview.html.f23b8be3.js new file mode 100644 index 000000000..c51f36927 --- /dev/null +++ b/assets/overview.html.f23b8be3.js @@ -0,0 +1 @@ +import{_ as i,y as d,z as c,X as t,B as e,Q as a,$ as o,P as r}from"./framework.fe9a73df.js";const u={},h=t("h1",{id:"vue-overview",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#vue-overview","aria-hidden":"true"},"#"),e(" Vue Overview")],-1),p=t("code",null,"Vue",-1),m={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},_={class:"table-of-contents"},f=t("h2",{id:"getting-started",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#getting-started","aria-hidden":"true"},"#"),e(" Getting Started")],-1),y=t("h2",{id:"typescript-layers",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#typescript-layers","aria-hidden":"true"},"#"),e(" TypeScript Layers")],-1),v={href:"https://www.npmjs.com/package/coalesce-vue",target:"_blank",rel:"noopener noreferrer"},g=t("img",{src:"https://img.shields.io/npm/v/coalesce-vue/latest?color=42b883&label=coalesce-vue%40latest",alt:""},null,-1),w={href:"https://www.npmjs.com/package/coalesce-vue",target:"_blank",rel:"noopener noreferrer"},k={href:"https://www.nuget.org/packages/IntelliTect.Coalesce/",target:"_blank",rel:"noopener noreferrer"},b={href:"https://www.npmjs.com/package/coalesce-vue",target:"_blank",rel:"noopener noreferrer"},V={id:"metadata-layer",tabindex:"-1"},T=t("a",{class:"header-anchor",href:"#metadata-layer","aria-hidden":"true"},"#",-1),x=t("p",null,[e("The metadata layer, generated as "),t("code",null,"metadata.g.ts"),e(", contains information about the types, properties, methods, and other components of your data model. Because Vue applications are typically compiled into a set of static assets, it is necessary for the frontend code to have a representation of your data model as an analog to the "),t("code",null,"ReflectionRepository"),e(" that is available at runtime in your .NET app.")],-1),M={id:"model-layer",tabindex:"-1"},C=t("a",{class:"header-anchor",href:"#model-layer","aria-hidden":"true"},"#",-1),L=t("code",null,"models.g.ts",-1),P=t("code",null,"$metadata",-1),j={id:"api-client-layer",tabindex:"-1"},I=t("a",{class:"header-anchor",href:"#api-client-layer","aria-hidden":"true"},"#",-1),S=t("code",null,"api-clients.g.ts",-1),A={id:"viewmodel-layer",tabindex:"-1"},E=t("a",{class:"header-anchor",href:"#viewmodel-layer","aria-hidden":"true"},"#",-1),R=t("code",null,"viewmodels.g.ts",-1),N=t("p",null,"These classes provide a wide array of functionality that is useful when interacting with your data model through a user interface. The generated ViewModels are the primary way that Coalesce is used when developing a Vue application.",-1),B=t("h2",{id:"vue-components",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#vue-components","aria-hidden":"true"},"#"),e(" Vue Components")],-1),G={href:"https://www.npmjs.com/package/coalesce-vue-vuetify2",target:"_blank",rel:"noopener noreferrer"},D=t("img",{src:"https://img.shields.io/npm/v/coalesce-vue-vuetify2/latest?color=42b883&label=coalesce-vue-vuetify2%40latest",alt:""},null,-1),O={href:"https://www.npmjs.com/package/coalesce-vue-vuetify3",target:"_blank",rel:"noopener noreferrer"},F=t("img",{src:"https://img.shields.io/npm/v/coalesce-vue-vuetify3/latest?color=42b883&label=coalesce-vue-vuetify3%40latest",alt:""},null,-1),$={href:"https://vuejs.org/",target:"_blank",rel:"noopener noreferrer"},z={href:"https://vuetifyjs.com/",target:"_blank",rel:"noopener noreferrer"},Q={href:"https://www.npmjs.com/package/coalesce-vue-vuetify2",target:"_blank",rel:"noopener noreferrer"},W={href:"https://www.npmjs.com/package/coalesce-vue-vuetify3",target:"_blank",rel:"noopener noreferrer"},X=t("h2",{id:"admin-views",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#admin-views","aria-hidden":"true"},"#"),e(" Admin Views")],-1),q=t("code",null,"/admin/Person",-1),H=t("code",null,"/admin/Person/edit",-1),J=t("code",null,"Person",-1),K=t("code",null,"/admin/Person/edit/:id",-1),U=t("code",null,"Person",-1);function Y(Z,ee){const s=r("ExternalLinkIcon"),n=r("router-link"),l=r("RouterLink");return d(),c("div",null,[h,t("p",null,[e("The "),p,e(" stack for Coalesce has been designed from the ground up to be used to build modern web applications using current technologies like Vite or Webpack + Vue CLI, ES Modules, and more. It enables you to use all of the features of Vue.js, including building a SPA, and the ability to use modern component frameworks like "),t("a",m,[e("Vuetify"),a(s)]),e(".")]),t("nav",_,[t("ul",null,[t("li",null,[a(n,{to:"#getting-started"},{default:o(()=>[e("Getting Started")]),_:1})]),t("li",null,[a(n,{to:"#typescript-layers"},{default:o(()=>[e("TypeScript Layers")]),_:1}),t("ul",null,[t("li",null,[a(n,{to:"#metadata-layer"},{default:o(()=>[e("Metadata Layer")]),_:1})]),t("li",null,[a(n,{to:"#model-layer"},{default:o(()=>[e("Model Layer")]),_:1})]),t("li",null,[a(n,{to:"#api-client-layer"},{default:o(()=>[e("API Client Layer")]),_:1})]),t("li",null,[a(n,{to:"#viewmodel-layer"},{default:o(()=>[e("ViewModel Layer")]),_:1})])])]),t("li",null,[a(n,{to:"#vue-components"},{default:o(()=>[e("Vue Components")]),_:1})]),t("li",null,[a(n,{to:"#admin-views"},{default:o(()=>[e("Admin Views")]),_:1})])])]),f,t("p",null,[e("Check out "),a(l,{to:"/stacks/vue/getting-started.html"},{default:o(()=>[e("Getting Started with Vue")]),_:1}),e(" to learn how to get a new Coalesce Vue project up and running.")]),y,t("p",null,[t("a",v,[g,a(s)])]),t("p",null,[e("The generated code for the Vue stack all builds on the "),t("a",w,[e("coalesce-vue"),a(s)]),e(" NPM package which contains most of the core functionality of the Vue stack. Its version should generally be kept in sync with the "),t("a",k,[e("IntelliTect.Coalesce NuGet packages"),a(s)]),e(" in your project.")]),t("p",null,[e("Both the generated code and "),t("a",b,[e("coalesce-vue"),a(s)]),e(" are split into four layers, with each layer building on the layers underneath. From the bottom, these layers are:")]),t("h3",V,[T,e(),a(l,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("Metadata Layer")]),_:1})]),x,t("p",null,[a(l,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("Read more about the Metadata layer")]),_:1})]),t("h3",M,[C,e(),a(l,{to:"/stacks/vue/layers/models.html"},{default:o(()=>[e("Model Layer")]),_:1})]),t("p",null,[e("The model layer, generated as "),L,e(", contains a set of TypeScript interfaces that represent each client-exposed type in your data model. Each interface contains all the "),a(l,{to:"/modeling/model-components/properties.html"},{default:o(()=>[e("Properties")]),_:1}),e(" of that type, as well as a "),P,e(" property that references the "),a(l,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("metadata")]),_:1}),e(" object for that type. Enums and "),a(l,{to:"/modeling/model-components/data-sources.html"},{default:o(()=>[e("Data Sources")]),_:1}),e(" are also represented in the model layer.")]),t("p",null,[a(l,{to:"/stacks/vue/layers/models.html"},{default:o(()=>[e("Read more about the Model layer")]),_:1})]),t("h3",j,[I,e(),a(l,{to:"/stacks/vue/layers/api-clients.html"},{default:o(()=>[e("API Client Layer")]),_:1})]),t("p",null,[e("The API client layer, generated as "),S,e(", exports a class for each API controller that was generated for your data model. These classes are stateless and provide one method for each API endpoint. This includes both the standard set of endpoints created for "),a(l,{to:"/modeling/model-types/entities.html"},{default:o(()=>[e("Entity Models")]),_:1}),e(" and "),a(l,{to:"/modeling/model-types/dtos.html"},{default:o(()=>[e("Custom DTOs")]),_:1}),e(", as well as any custom "),a(l,{to:"/modeling/model-components/methods.html"},{default:o(()=>[e("Methods")]),_:1}),e(" on the aforementioned types, as well as any methods on your "),a(l,{to:"/modeling/model-types/services.html"},{default:o(()=>[e("Services")]),_:1}),e(".")]),t("p",null,[a(l,{to:"/stacks/vue/layers/api-clients.html"},{default:o(()=>[e("Read more about the API Client layer")]),_:1})]),t("h3",A,[E,e(),a(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("ViewModel Layer")]),_:1})]),t("p",null,[e("The ViewModel layer, generated as "),R,e(", exports a ViewModel class for each API-backed type in your data model ("),a(l,{to:"/modeling/model-types/entities.html"},{default:o(()=>[e("Entity Models")]),_:1}),e(", "),a(l,{to:"/modeling/model-types/dtos.html"},{default:o(()=>[e("Custom DTOs")]),_:1}),e(", and "),a(l,{to:"/modeling/model-types/services.html"},{default:o(()=>[e("Services")]),_:1}),e("). It also exports a ListViewModel type for "),a(l,{to:"/modeling/model-types/entities.html"},{default:o(()=>[e("Entity Models")]),_:1}),e(" and "),a(l,{to:"/modeling/model-types/dtos.html"},{default:o(()=>[e("Custom DTOs")]),_:1}),e(".")]),N,t("p",null,[a(l,{to:"/stacks/vue/layers/viewmodels.html"},{default:o(()=>[e("Read more about the ViewModel layer")]),_:1})]),B,t("p",null,[t("a",G,[D,a(s)]),t("a",O,[F,a(s)])]),t("p",null,[e("The "),t("a",$,[e("Vue"),a(s)]),e(" stack for Coalesce provides "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:o(()=>[e("a set of components")]),_:1}),e(" based on "),t("a",z,[e("Vuetify"),a(s)]),e(", packaged up in an NPM package "),t("a",Q,[e("coalesce-vue-vuetify2"),a(s)]),e(" or "),t("a",W,[e("coalesce-vue-vuetify3"),a(s)]),e(". These components are driven primarily by the "),a(l,{to:"/stacks/vue/layers/metadata.html"},{default:o(()=>[e("Metadata Layer")]),_:1}),e(", and include both low level input and display components like "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-input.html"},{default:o(()=>[e("c-input")]),_:1}),e(" and "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-display.html"},{default:o(()=>[e("c-display")]),_:1}),e(" that are highly reusable in the custom pages you'll build in your application, as well as high-level components like "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html"},{default:o(()=>[e("c-admin-table-page")]),_:1}),e(" and "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html"},{default:o(()=>[e("c-admin-editor-page")]),_:1}),e(" that constitute entire pages.")]),t("p",null,[a(l,{to:"/stacks/vue/coalesce-vue-vuetify/overview.html"},{default:o(()=>[e("Read more about the Vuetify Components here")]),_:1}),e(".")]),X,t("p",null,[e("The Vue.js stack for Coalesce provides some high level components that provide functionality of whole pages like "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html"},{default:o(()=>[e("c-admin-table-page")]),_:1}),e(" and "),a(l,{to:"/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html"},{default:o(()=>[e("c-admin-editor-page")]),_:1}),e(".")]),t("p",null,[e("The template described in "),a(l,{to:"/stacks/vue/getting-started.html"},{default:o(()=>[e("Getting Started with Vue")]),_:1}),e(" comes with routes already in place for these page-level components. For example, "),q,e(" for a table, "),H,e(" to create a new "),J,e(", and "),K,e(" to edit a "),U,e(".")])])}const ae=i(u,[["render",Y],["__file","overview.html.vue"]]);export{ae as default}; diff --git a/assets/overview.html.f822ebee.js b/assets/overview.html.f822ebee.js new file mode 100644 index 000000000..4245efc82 --- /dev/null +++ b/assets/overview.html.f822ebee.js @@ -0,0 +1 @@ +import{_ as l,y as r,z as h,X as t,B as e,Q as o,$ as a,a5 as c,P as d}from"./framework.fe9a73df.js";const u={},m=t("h1",{id:"knockout-overview",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#knockout-overview","aria-hidden":"true"},"#"),e(" Knockout Overview")],-1),f={href:"https://knockoutjs.com/",target:"_blank",rel:"noopener noreferrer"},p={href:"https://knockoutjs.com/",target:"_blank",rel:"noopener noreferrer"},_={class:"table-of-contents"},g=t("h2",{id:"getting-started",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#getting-started","aria-hidden":"true"},"#"),e(" Getting Started")],-1),y=t("h2",{id:"generated-code",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#generated-code","aria-hidden":"true"},"#"),e(" Generated Code")],-1),w=t("p",null,"Below you will find a brief overview of each of the different pieces of code that Coalesce will generate for you when you choose the Knockout stack.",-1),v=t("h3",{id:"typescript",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#typescript","aria-hidden":"true"},"#"),e(" TypeScript")],-1),b=t("p",null,"Coalesce generates a number of different types of TypeScript classes to support your data through the generated API.",-1),k={id:"viewmodels",tabindex:"-1"},x=t("a",{class:"header-anchor",href:"#viewmodels","aria-hidden":"true"},"#",-1),T=t("code",null,"BaseViewModel",-1),C={id:"list-viewmodels",tabindex:"-1"},M=t("a",{class:"header-anchor",href:"#list-viewmodels","aria-hidden":"true"},"#",-1),V={id:"external-type-viewmodels",tabindex:"-1"},S=t("a",{class:"header-anchor",href:"#external-type-viewmodels","aria-hidden":"true"},"#",-1),E=t("code",null,"KnockoutObservable",-1),P=t("h3",{id:"view-controllers",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#view-controllers","aria-hidden":"true"},"#"),e(" View Controllers")],-1),O=t("code",null,"/Controllers/Generated",-1),A=t("p",null,[e("As you add your own pages to your application, you should add additional partial classes in the "),t("code",null,"/Controllers"),e(" that extend these generated partial classes to expose those pages.")],-1),K=t("h3",{id:"admin-views",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#admin-views","aria-hidden":"true"},"#"),e(" Admin Views")],-1),L=c('

    Table

    Provides a basic table view with sorting, searching, and paging of your data. Can be rendered in either read-only mode (routed as /Table), or editable mode (routed as TableEdit).

    Cards

    Provides a card-based view of your data with searching and paging.

    CreateEdit

    Provides an editor view which can be used to create new entities or edit existing ones.

    EditorHtml

    Provides a minimal amount of HTML to display an editor for the object type. This is used by the showEditor method on the generated TypeScript ViewModels.

    ',8);function j(B,G){const s=d("ExternalLinkIcon"),i=d("RouterLink"),n=d("router-link");return r(),h("div",null,[m,t("p",null,[e("The "),t("a",f,[e("Knockout"),o(s)]),e(" stack for Coalesce offers the ability to build pages with the time-tested "),t("a",p,[e("Knockout"),o(s)]),e(" JavaScript library using all of the features of the Coalesce generated APIs and "),o(i,{to:"/stacks/disambiguation/view-model.html"},{default:a(()=>[e("ViewModels")]),_:1}),e(". It can be used for anything between adding simple interactive augmentations of MVC pages to building a full MPA-SPA hybrid application.")]),t("nav",_,[t("ul",null,[t("li",null,[o(n,{to:"#getting-started"},{default:a(()=>[e("Getting Started")]),_:1})]),t("li",null,[o(n,{to:"#generated-code"},{default:a(()=>[e("Generated Code")]),_:1}),t("ul",null,[t("li",null,[o(n,{to:"#typescript"},{default:a(()=>[e("TypeScript")]),_:1})]),t("li",null,[o(n,{to:"#view-controllers"},{default:a(()=>[e("View Controllers")]),_:1})]),t("li",null,[o(n,{to:"#admin-views"},{default:a(()=>[e("Admin Views")]),_:1})])])])])]),g,t("p",null,[e("Check out "),o(i,{to:"/stacks/ko/getting-started.html"},{default:a(()=>[e("Getting Started with Knockout")]),_:1}),e(" if you haven't already to learn how to get a new Coalesce Knockout project up and running.")]),y,w,v,b,t("h4",k,[x,e(),o(i,{to:"/stacks/ko/client/view-model.html"},{default:a(()=>[e("ViewModels")]),_:1})]),t("p",null,[e("One view model class is generated for each of your "),o(i,{to:"/modeling/model-types/entities.html"},{default:a(()=>[e("Entity Models")]),_:1}),e(" and "),o(i,{to:"/modeling/model-types/dtos.html"},{default:a(()=>[e("Custom DTOs")]),_:1}),e(". These models contain fields for your model "),o(i,{to:"/modeling/model-components/properties.html"},{default:a(()=>[e("Properties")]),_:1}),e(", and functions and other members for your model "),o(i,{to:"/modeling/model-components/methods.html"},{default:a(()=>[e("Methods")]),_:1}),e(". They also contain a number of standard fields & functions inherited from "),T,e(" which offer basic loading & saving functionality, as well as other handy utility members for use with Knockout.")]),t("p",null,[e("See "),o(i,{to:"/stacks/ko/client/view-model.html"},{default:a(()=>[e("TypeScript ViewModels")]),_:1}),e(" for more details.")]),t("h4",C,[M,e(),o(i,{to:"/stacks/ko/client/list-view-model.html"},{default:a(()=>[e("List ViewModels")]),_:1})]),t("p",null,[e("One ListViewModel is generated for each of your "),o(i,{to:"/modeling/model-types/entities.html"},{default:a(()=>[e("Entity Models")]),_:1}),e(" and "),o(i,{to:"/modeling/model-types/dtos.html"},{default:a(()=>[e("Custom DTOs")]),_:1}),e(". These classes contain functionality for loading sets of objects from the server. They provide searching, paging, sorting, and filtering functionality.")]),t("p",null,[e("See "),o(i,{to:"/stacks/ko/client/list-view-model.html"},{default:a(()=>[e("TypeScript ListViewModels")]),_:1}),e(" for more details.")]),t("h4",V,[S,e(),o(i,{to:"/stacks/ko/client/external-view-model.html"},{default:a(()=>[e("External Type ViewModels")]),_:1})]),t("p",null,[e("Any non-primitive types which are not themselves a "),o(i,{to:"/modeling/model-types/entities.html"},{default:a(()=>[e("Entity Models")]),_:1}),e(" or "),o(i,{to:"/modeling/model-types/dtos.html"},{default:a(()=>[e("Custom DTOs")]),_:1}),e(" which are accessible through the aforementioned types, either through one of its "),o(i,{to:"/modeling/model-components/properties.html"},{default:a(()=>[e("Properties")]),_:1}),e(", or return value from one of its "),o(i,{to:"/modeling/model-components/methods.html"},{default:a(()=>[e("Methods")]),_:1}),e(", will have a corresponding TypeScript ViewModel generated for it. These ViewModels only provide a "),E,e(" field for each property on the C# class.")]),t("p",null,[e("See "),o(i,{to:"/stacks/ko/client/external-view-model.html"},{default:a(()=>[e("TypeScript External ViewModels")]),_:1}),e(" for more details.")]),P,t("p",null,[e("For each of your "),o(i,{to:"/modeling/model-types/entities.html"},{default:a(()=>[e("Entity Models")]),_:1}),e(" and "),o(i,{to:"/modeling/model-types/dtos.html"},{default:a(()=>[e("Custom DTOs")]),_:1}),e(", a controller is created in the "),O,e(" directory of your web project. These controllers provide routes for the generated admin views.")]),A,K,t("p",null,[e("For each of your "),o(i,{to:"/modeling/model-types/entities.html"},{default:a(()=>[e("Entity Models")]),_:1}),e(" and "),o(i,{to:"/modeling/model-types/dtos.html"},{default:a(()=>[e("Custom DTOs")]),_:1}),e(", a number of views are generated to provide administrative-level access to your data.")]),L])}const I=l(u,[["render",j],["__file","overview.html.vue"]]);export{I as default}; diff --git a/assets/properties.html.bcebe6c1.js b/assets/properties.html.bcebe6c1.js new file mode 100644 index 000000000..53d1551f6 --- /dev/null +++ b/assets/properties.html.bcebe6c1.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-57b8d80e","path":"/modeling/model-components/properties.html","title":"Properties","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Property Varieties","slug":"property-varieties","link":"#property-varieties","children":[{"level":3,"title":"Primary Key","slug":"primary-key","link":"#primary-key","children":[]},{"level":3,"title":"Foreign Keys & Reference Navigation Properties","slug":"foreign-keys-reference-navigation-properties","link":"#foreign-keys-reference-navigation-properties","children":[]},{"level":3,"title":"Collection Navigation Properties","slug":"collection-navigation-properties","link":"#collection-navigation-properties","children":[]},{"level":3,"title":"Non-mapped POCOs","slug":"non-mapped-pocos","link":"#non-mapped-pocos","children":[]},{"level":3,"title":"Primitives, Scalars, & Dates","slug":"primitives-scalars-dates","link":"#primitives-scalars-dates","children":[]},{"level":3,"title":"Getter-only Properties","slug":"getter-only-properties","link":"#getter-only-properties","children":[]},{"level":3,"title":"Init-only Properties","slug":"init-only-properties","link":"#init-only-properties","children":[]}]},{"level":2,"title":"Other Considerations","slug":"other-considerations","link":"#other-considerations","children":[{"level":3,"title":"Attributes","slug":"attributes","link":"#attributes","children":[]},{"level":3,"title":"Security","slug":"security","link":"#security","children":[]},{"level":3,"title":"Loading & Serialization","slug":"loading-serialization","link":"#loading-serialization","children":[]},{"level":3,"title":"NotMapped","slug":"notmapped","link":"#notmapped","children":[]}]}],"git":{"updatedTime":1680213338000},"filePathRelative":"modeling/model-components/properties.md"}');export{e as data}; diff --git a/assets/properties.html.e0250356.js b/assets/properties.html.e0250356.js new file mode 100644 index 000000000..77924ed79 --- /dev/null +++ b/assets/properties.html.e0250356.js @@ -0,0 +1 @@ +import{_ as s,y as d,z as l,X as t,B as e,Q as o,$ as i,a5 as h,P as n}from"./framework.fe9a73df.js";const p={},c=h('

    Properties

    Models in a Coalesce application are just EF Core POCOs. The properties defined on your models should fit within the constraints of EF Core.

    Coalesce currently has a few more restrictions than what EF Core allows, but hopefully over time some of these restrictions can be relaxed as Coalesce grows in capability.

    Property Varieties

    The following kinds of properties may be declared on your models.

    Primary Key

    To work with Coalesce, your model must have a single property for a primary key. By convention, this property should be named the same as your model class with Id appended to that name, but you can also annotate a property with [Key] to denote it as the primary key.

    Foreign Keys & Reference Navigation Properties

    While a foreign key may be declared on your model using only the EF OnModuleBuilding method to specify its purpose, Coalesce won't know what the property is a key for. Therefore, foreign key properties should always be accompanied by a reference navigation property, and vice versa.

    ',9),u=t("code",null,'"Id"',-1),m=t("code",null,"[ForeignKeyAttribute]",-1),y={href:"https://learn.microsoft.com/en-us/ef/core/modeling/relationships/mapping-attributes#foreignkeyattribute",target:"_blank",rel:"noopener noreferrer"},f=t("h3",{id:"collection-navigation-properties",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#collection-navigation-properties","aria-hidden":"true"},"#"),e(" Collection Navigation Properties")],-1),b=t("code",null,"[InversePropertyAttribute]",-1),_={href:"https://learn.microsoft.com/en-us/ef/core/modeling/relationships/mapping-attributes#inversepropertyattribute",target:"_blank",rel:"noopener noreferrer"},g=t("code",null,"ICollection",-1),v=t("h3",{id:"non-mapped-pocos",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#non-mapped-pocos","aria-hidden":"true"},"#"),e(" Non-mapped POCOs")],-1),w=t("code",null,"DbContext",-1),k=t("h3",{id:"primitives-scalars-dates",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#primitives-scalars-dates","aria-hidden":"true"},"#"),e(" Primitives, Scalars, & Dates")],-1),x=t("p",null,[e("Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, "),t("code",null,"DateTime"),e(", "),t("code",null,"DateTimeOffset"),e("), and their nullable variants, are all supported as model properties.")],-1),C=t("h3",{id:"getter-only-properties",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#getter-only-properties","aria-hidden":"true"},"#"),e(" Getter-only Properties")],-1),E=t("p",null,[e("If such a property is defined as an auto-property, the "),t("code",null,"[NotMapped]"),e(" attribute should be used to prevent EF Core from attempting to map such a property to your database.")],-1),P=t("h3",{id:"init-only-properties",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#init-only-properties","aria-hidden":"true"},"#"),e(" Init-only Properties")],-1),T=t("code",null,"init",-1),F=t("code",null,"set",-1),I=t("h2",{id:"other-considerations",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#other-considerations","aria-hidden":"true"},"#"),e(" Other Considerations")],-1),M=t("p",null,"For any of the kinds of properties outlined above, the following rules are applied:",-1),N=t("h3",{id:"attributes",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#attributes","aria-hidden":"true"},"#"),e(" Attributes")],-1),S=t("h3",{id:"security",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#security","aria-hidden":"true"},"#"),e(" Security")],-1),D=t("h3",{id:"loading-serialization",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#loading-serialization","aria-hidden":"true"},"#"),e(" Loading & Serialization")],-1),V=t("h3",{id:"notmapped",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#notmapped","aria-hidden":"true"},"#"),e(" NotMapped")],-1),O=t("p",null,[e("While Coalesce does not do anything special for the "),t("code",null,"[NotMapped]"),e(" attribute, it is still and important attribute to keep in mind while building your model, as it prevents EF Core from doing anything with the property.")],-1);function B(z,A){const a=n("ExternalLinkIcon"),r=n("RouterLink");return d(),l("div",null,[c,t("p",null,[e("In cases where the foreign key is not named after the navigation property with "),u,e(" appended, the "),m,e(" may be used on either the key or the navigation property to denote the other property of the pair, in accordance with the recommendations set forth by "),t("a",y,[e("EF Core's Modeling Guidelines"),o(a)]),e(".")]),f,t("p",null,[e("Collection navigation properties can be used in a straightforward manner. In the event where the inverse property on the other side of the relationship cannot be determined, "),b,e(" will need to be used. "),t("a",_,[e("EF Core provides documentation"),o(a)]),e(" on how to use this attribute. Errors will be displayed at generation time if an inverse property cannot be determined without the attribute. We recommend recommended that you declare the type of collection navigation properties as "),g,e(".")]),v,t("p",null,[e("Properties of a type that are not on your "),w,e(" will also have corresponding properties generated on the "),o(r,{to:"/stacks/disambiguation/view-model.html"},{default:i(()=>[e("TypeScript ViewModels")]),_:1}),e(" typed as "),o(r,{to:"/stacks/disambiguation/external-view-model.html"},{default:i(()=>[e("TypeScript External ViewModels")]),_:1}),e(", and the values of such properties will be sent with the object to the client when requested. Properties of this type will also be sent back to the server by the client when they are encountered (currently supported by the "),o(r,{to:"/stacks/vue/overview.html"},{default:i(()=>[e("Vue Stack")]),_:1}),e(" only).")]),t("p",null,[e("See "),o(r,{to:"/modeling/model-types/external-types.html"},{default:i(()=>[e("External Types")]),_:1}),e(" for more information.")]),k,x,C,t("p",null,[e("Any property that only has a getter will also have a corresponding property generated in the "),o(r,{to:"/stacks/disambiguation/view-model.html"},{default:i(()=>[e("TypeScript ViewModels")]),_:1}),e(", but won't be sent back to the server during any save actions.")]),E,P,t("p",null,[e("Properties on "),o(r,{to:"/modeling/model-types/entities.html"},{default:i(()=>[e("Entity Models")]),_:1}),e(" that use an "),T,e(" accessor rather than a "),F,e(" accessor will be implicitly treated as required, and can also only have a value provided when the entity is created for the first time. Any values provided during save actions for init-only properties when updating an existing entity will be ignored.")]),I,M,N,t("p",null,[e("Coalesce provides a number of "),o(r,{to:"/modeling/model-components/attributes.html"},{default:i(()=>[e("Attributes")]),_:1}),e(", and supports a number of other .NET attributes, that allow for further customization of your model.")]),S,t("p",null,[e("Properties will be ignored if received by the client if authorization checks against any "),o(r,{to:"/topics/security.html#property-column-security"},{default:i(()=>[e("property-level Security")]),_:1}),e(" present fail. This security is handled by the "),o(r,{to:"/stacks/agnostic/dtos.html"},{default:i(()=>[e("Generated C# DTOs")]),_:1}),e(".")]),D,t("p",null,[e("The "),o(r,{to:"/modeling/model-components/data-sources.html#default-loading-behavior"},{default:i(()=>[e("Default Loading Behavior")]),_:1}),e(", any custom functionality defined in "),o(r,{to:"/modeling/model-components/data-sources.html"},{default:i(()=>[e("Data Sources")]),_:1}),e(", and "),o(r,{to:"/modeling/model-components/attributes/dto-includes-excludes.html"},{default:i(()=>[e("[DtoIncludes] & [DtoExcludes]")]),_:1}),e(" may also restrict which properties are sent to the client when requested.")]),V,O])}const K=s(p,[["render",B],["__file","properties.html.vue"]]);export{K as default}; diff --git a/assets/search.html.6125f4c3.js b/assets/search.html.6125f4c3.js new file mode 100644 index 000000000..fd99ae455 --- /dev/null +++ b/assets/search.html.6125f4c3.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-d3094c1e","path":"/modeling/model-components/attributes/search.html","title":"[Search]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Searchable Property Types","slug":"searchable-property-types","link":"#searchable-property-types","children":[]},{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/search.md"}');export{e as data}; diff --git a/assets/search.html.9174939a.js b/assets/search.html.9174939a.js new file mode 100644 index 000000000..accf6d8e4 --- /dev/null +++ b/assets/search.html.9174939a.js @@ -0,0 +1,19 @@ +import{_ as r,y as i,z as c,Q as s,X as e,B as a,a5 as t,P as o}from"./framework.fe9a73df.js";const p={},d=t(`

    [Search]

    Coalesce supports searching through the generated API in its various implementations, including the generated list views (Table & Cards), in Select2 dropdowns, and directly through the TypeScript ListViewModels' search property.

    The search parameter of the API can also be formatted as PropertyName:SearchTerm in order to search on an arbitrary property of a model. For example, a value of Nickname:Steve-o for a search term would search the Nickname property, even through it is not marked as searchable using this attribute.

    By default, the system will search any field with the name 'Name'. If this doesn't exist, the ID is used as the only searchable field. Once you place the Search attribute on one or more properties on a model, only those annotated properties will be searched.

    Searchable Property Types

    Strings

    String fields will be searched based on the SearchMethod property on the attribute. See below.

    Numeric Types

    If the input is numeric, numeric fields will be searched for the exact value.

    Enums

    If the input is a valid name of an enum value for an enum property and that property is searchable, rows will be searched for the exact value.

    Dates

    If the input is a parsable date, rows will be searched based on that date.

    Date search will do its best to guess at the user's intentions:

    • Various forms of year/month combos are supported, and if only a year/month is inputted, it will look for all dates in that month, e.g. "Feb 2017" or "2016-11".
    • A date without a time (or a time of exactly midnight) will search the entire day, e.g. "2017/4/18".
    • A date/time with minutes and seconds equal to 0 will search the entire hour, e.g. "April 7, 2017 11 AM".

    TIP

    When searching on date properties, you should almost always set IsSplitOnSpaces = false on the Search attribute. This allows natural inputs like "July 21, 2017" to search correctly. Otherwise, only non-whitespace date formats will work, like "2017/21/07".

    Reference Navigation Properties

    When a reference navigation property is marked with [Search], searchable properties on the referenced object will also be searched. This behavior will go up to two levels away from the root object, and can be controlled with the RootWhitelist and RootBlacklist properties on the [Search] attribute that are outlined below.

    Collection Navigation Properties

    When a collection navigation property is marked with [Search], searchable properties on the child objects will also be searched. This behavior will go up to two levels away from the root object, and can be controlled with the RootWhitelist and RootBlacklist properties on the [Search] attribute that are outlined below.

    WARNING

    Searches on collection navigation properties usually don't translate well with EF Core, leading to potentially degraded performance. Use this feature cautiously.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    [Search]
    +    public string FirstName { get; set; }
    +
    +    [Search]
    +    public string LastName { get; set; }
    +
    +    [Search(IsSplitOnSpaces = false)]
    +    public string BirthDate { get; set; }
    +
    +    public string Nickname { get; set; }
    +
    +    [Search(RootWhitelist = nameof(Person))]
    +    public ICollection<Address> Addresses { get; set; }
    +}
    +

    Properties

    `,24),h=e("p",null,[a("If set to true (the default), each word in the search terms will be searched for in each searchable field independently, and a row will only be considered a match if each word in the search term is a match on at least one searchable property where "),e("code",null,"IsSplitOnSpaces == true")],-1),D=e("code",null,"IsSplitOnSpaces = true",-1),u={href:"https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/",target:"_blank",rel:"noopener noreferrer"},y=t("

    For string properties, specifies how the value in the property/column will be matched.

    • BeginsWith: Search term will be checked for at the beginning of the field's value in a case insensitive manner.
    • Equals: Search term must match the field exactly in a case insensitive manner.
    • EqualsNatural: Search term must match exactly, using the natural casing handling of the evaluation environment. Default database collation will be used if evaluated in SQL, and exact casing will be used if evaluated in memory. This allows index seeks to be used instead of index scans, providing extra high performance searches against indexed columns
    • Contains: Search term will be checked for anywhere inside the field's value in a case insensitive manner. Will be slow against large databases - performance cannot be improved with database indexing.
    ",2),m=e("p",null,"A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched unless the root object of the API call was one of the specified class names.",-1),b=e("p",null,"A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched if the root object of the API call was one of the specified class names.",-1);function f(v,g){const n=o("Prop"),l=o("ExternalLinkIcon");return i(),c("div",null,[d,s(n,{def:"public bool IsSplitOnSpaces { get; set; } = true;"}),h,e("p",null,[a("This is useful when searching for a full name across two or more fields. In the above example, using "),D,a(" would provide more intuitive behavior since it will search both first name and last name for each word entered into the search field. But, "),e("a",u,[a("you probably shouldn't be doing that"),s(l)]),a(".")]),s(n,{def:"public SearchMethods SearchMethod { get; set; } = SearchMethods.BeginsWith;"}),y,s(n,{def:"public string RootWhitelist { get; set; } = null;"}),m,s(n,{def:"public string RootBlacklist { get; set; } = null;"}),b])}const C=r(p,[["render",f],["__file","search.html.vue"]]);export{C as default}; diff --git a/assets/security-attribute.html.6ed22e0c.js b/assets/security-attribute.html.6ed22e0c.js new file mode 100644 index 000000000..59eede362 --- /dev/null +++ b/assets/security-attribute.html.6ed22e0c.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-492e3baa","path":"/modeling/model-components/attributes/security-attribute.html","title":"Security Attributes","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Class vs. Property Security","slug":"class-vs-property-security","link":"#class-vs-property-security","children":[]},{"level":2,"title":"Implementations","slug":"implementations","link":"#implementations","children":[{"level":3,"title":"[Read]","slug":"read","link":"#read","children":[]},{"level":3,"title":"[Edit]","slug":"edit","link":"#edit","children":[]},{"level":3,"title":"[Create]","slug":"create","link":"#create","children":[]},{"level":3,"title":"[Delete]","slug":"delete","link":"#delete","children":[]},{"level":3,"title":"[Execute]","slug":"execute","link":"#execute","children":[]}]},{"level":2,"title":"Attribute Properties","slug":"attribute-properties","link":"#attribute-properties","children":[]}],"git":{"updatedTime":1680571733000},"filePathRelative":"modeling/model-components/attributes/security-attribute.md"}');export{e as data}; diff --git a/assets/security-attribute.html.aaba8860.js b/assets/security-attribute.html.aaba8860.js new file mode 100644 index 000000000..33808775b --- /dev/null +++ b/assets/security-attribute.html.aaba8860.js @@ -0,0 +1,31 @@ +import{_ as i,y as c,z as d,X as e,B as s,Q as a,$ as n,a5 as r,P as t}from"./framework.fe9a73df.js";const D={},u=e("h1",{id:"security-attributes",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#security-attributes","aria-hidden":"true"},"#"),s(" Security Attributes")],-1),y=e("p",null,"Coalesce provides a collection of four attributes which can provide class-level (and property-level, where appropriate) security controls over the generated API.",-1),h={class:"custom-container tip"},m=e("p",{class:"custom-container-title"},"TIP",-1),v={class:"table-of-contents"},b=e("h2",{id:"class-vs-property-security",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#class-vs-property-security","aria-hidden":"true"},"#"),s(" Class vs. Property Security")],-1),C=e("code",null,"[Authorize]",-1),f=r('

    Implementations

    [Read]

    Controls permissions for reading of objects and properties through the API.

    For property-level security only, if a [Read] attribute is present without an [Edit] attribute, the property is read-only.

    ',4),g=e("code",null,"NoAutoInclude = true",-1),E=e("code",null,"[Read]",-1),_=r(`

    Example Usage

    [Read(Roles = "Management", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +
    +    [Read("Payroll")]
    +    public string LastFourSsn { get; set; }
    +    
    +    ...
    +}
    +

    [Edit]

    Controls permissions for editing of objects and properties through the API.

    For property-level security only, if a [Read] attribute is present, one of its roles must be fulfilled in addition to the roles specified (if any) for the [Edit] attribute.

    Example Usage

    [Edit(Roles = "Management,Payroll", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +
    +    [Read("Payroll,HumanResources"), Edit("Payroll")]
    +    public string LastFourSsn { get; set; }
    +    
    +    ...
    +}
    +

    [Create]

    Controls permissions for creation of an object of the targeted type through the API.

    Example Usage

    [Create(Roles = "HumanResources", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    ...
    +}
    +

    [Delete]

    Controls permissions for deletion of an object of the targeted type through the API.

    Example Usage

    [Delete(Roles = "HumanResources,Management", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    ...
    +}
    +

    [Execute]

    `,16),x=e("h2",{id:"attribute-properties",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#attribute-properties","aria-hidden":"true"},"#"),s(" Attribute Properties")],-1),A=e("p",null,"A comma-delimited list of roles that are authorized to take perform the action represented by the attribute. If the current user belongs to any of the listed roles, the action will be allowed.",-1),P=e("p",null,[s("The string set for this property will be outputted as an "),e("code",null,'[Authorize(Roles="RolesString")]'),s(" attribute on generated API controller actions.")],-1),F=r("

    The level of access to allow for the action for class-level security only. Has no effect for property-level security.

    Enum values are:

    • SecurityPermissionLevels.AllowAll Allow all users to perform the action for the attribute, including users who are not authenticated at all.
    • SecurityPermissionLevels.AllowAuthorized Allow only users who are members of the roles specified on the attribute to perform the action. If no roles are specified on the attribute, then all authenticated users are allowed (no anonymous access).
    • SecurityPermissionLevels.DenyAll Deny the action to all users, regardless of authentication status or authorization level. If DenyAll is used, no API endpoint for the action will be generated.
    ",3);function w(I,R){const o=t("RouterLink"),l=t("router-link"),p=t("Prop");return c(),d("div",null,[u,y,e("div",h,[m,e("p",null,[s("This page provides API-level documentation for a specific set of four attributes. For a complete overview of all the security-focused techniques that can be used in a Coalesce application, see the "),a(o,{to:"/topics/security.html"},{default:n(()=>[s("Security")]),_:1}),s(" page.")])]),e("nav",v,[e("ul",null,[e("li",null,[a(l,{to:"#class-vs-property-security"},{default:n(()=>[s("Class vs. Property Security")]),_:1})]),e("li",null,[a(l,{to:"#implementations"},{default:n(()=>[s("Implementations")]),_:1}),e("ul",null,[e("li",null,[a(l,{to:"#read"},{default:n(()=>[s("[Read]")]),_:1})]),e("li",null,[a(l,{to:"#edit"},{default:n(()=>[s("[Edit]")]),_:1})]),e("li",null,[a(l,{to:"#create"},{default:n(()=>[s("[Create]")]),_:1})]),e("li",null,[a(l,{to:"#delete"},{default:n(()=>[s("[Delete]")]),_:1})]),e("li",null,[a(l,{to:"#execute"},{default:n(()=>[s("[Execute]")]),_:1})])])]),e("li",null,[a(l,{to:"#attribute-properties"},{default:n(()=>[s("Attribute Properties")]),_:1})])])]),b,e("p",null,[s("There are important differences between class-level security and property-level security, beyond the usage of the attributes themselves. In general, class-level security is implemented in the generated API Controllers as "),C,s(" attributes on the generated actions. Property security attributes are implemented in the "),a(o,{to:"/stacks/agnostic/dtos.html"},{default:n(()=>[s("Generated C# DTOs")]),_:1}),s(".")]),f,e("p",null,[s("Additionally, you can set "),g,s(" the "),E,s(" attribute to suppress the "),a(o,{to:"/modeling/model-components/data-sources.html#default-loading-behavior"},{default:n(()=>[s("Default Loading Behavior")]),_:1}),s(".")]),_,e("p",null,[s("A separate attribute for controlling method execution exists. Its documentation may be found on the "),a(o,{to:"/modeling/model-components/attributes/execute.html"},{default:n(()=>[s("[Execute]")]),_:1}),s(" page.")]),x,a(p,{def:"public string Roles { get; set; }",ctor:"1"}),s(),A,P,a(p,{def:"public SecurityPermissionLevels PermissionLevel { get; set; }",ctor:"2"}),s(),F])}const k=i(D,[["render",w],["__file","security-attribute.html.vue"]]);export{k as default}; diff --git a/assets/security-overview.671c396c.webp b/assets/security-overview.671c396c.webp new file mode 100644 index 000000000..428048aba Binary files /dev/null and b/assets/security-overview.671c396c.webp differ diff --git a/assets/security.html.4972855d.js b/assets/security.html.4972855d.js new file mode 100644 index 000000000..f6fe8a9b7 --- /dev/null +++ b/assets/security.html.4972855d.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-74ba1c67","path":"/topics/security.html","title":"Security","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Endpoint Security","slug":"endpoint-security","link":"#endpoint-security","children":[{"level":3,"title":"Standard CRUD Endpoints","slug":"standard-crud-endpoints","link":"#standard-crud-endpoints","children":[]},{"level":3,"title":"Custom Methods and Services","slug":"custom-methods-and-services","link":"#custom-methods-and-services","children":[]}]},{"level":2,"title":"Property/Column Security","slug":"property-column-security","link":"#property-column-security","children":[{"level":3,"title":"Internal Properties","slug":"internal-properties","link":"#internal-properties","children":[]},{"level":3,"title":"Attributes","slug":"attributes","link":"#attributes","children":[]},{"level":3,"title":"Read-Only Properties","slug":"read-only-properties","link":"#read-only-properties","children":[]},{"level":3,"title":"Read/Write Properties","slug":"read-write-properties","link":"#read-write-properties","children":[]}]},{"level":2,"title":"Row-level Security","slug":"row-level-security","link":"#row-level-security","children":[{"level":3,"title":"Data Sources","slug":"data-sources","link":"#data-sources","children":[]},{"level":3,"title":"EF Global Query Filters","slug":"ef-global-query-filters","link":"#ef-global-query-filters","children":[]},{"level":3,"title":"Foreign Key Injection Vulnerabilities","slug":"foreign-key-injection-vulnerabilities","link":"#foreign-key-injection-vulnerabilities","children":[]}]},{"level":2,"title":"Server-side Data Validation","slug":"server-side-data-validation","link":"#server-side-data-validation","children":[{"level":3,"title":"Attribute Validation","slug":"attribute-validation","link":"#attribute-validation","children":[]},{"level":3,"title":"Saves and Deletes","slug":"saves-and-deletes","link":"#saves-and-deletes","children":[]},{"level":3,"title":"Custom Methods and Services","slug":"custom-methods-and-services-1","link":"#custom-methods-and-services-1","children":[]}]},{"level":2,"title":"Security Overview Page","slug":"security-overview-page","link":"#security-overview-page","children":[]},{"level":2,"title":"Testing Your Security","slug":"testing-your-security","link":"#testing-your-security","children":[]}],"git":{"updatedTime":1694448005000},"filePathRelative":"topics/security.md"}');export{e as data}; diff --git a/assets/security.html.df29655c.js b/assets/security.html.df29655c.js new file mode 100644 index 000000000..3c97959ac --- /dev/null +++ b/assets/security.html.df29655c.js @@ -0,0 +1,276 @@ +import{_ as D,y as c,z as i,X as n,Q as a,$ as e,B as s,a5 as t,P as r}from"./framework.fe9a73df.js";const y="/Coalesce/assets/security-overview.671c396c.webp",d={},u=n("h1",{id:"security",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#security","aria-hidden":"true"},"#"),s(" Security")],-1),C=n("p",null,"This page is a comprehensive overview of all the techniques that can be used in a Coalesce application to restrict the usage of API endpoints that Coalesce generates.",-1),h={class:"table-of-contents"},m=n("h2",{id:"endpoint-security",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#endpoint-security","aria-hidden":"true"},"#"),s(" Endpoint Security")],-1),b=n("code",null,"[Coalesce]",-1),E=n("code",null,"DbContext",-1),f=n("p",null,[s("Classes can be hidden from Coalesce entirely by annotating them with "),n("code",null,"[InternalUse]"),s(", preventing generation of API endpoints for that class, as well as preventing properties of that type from being exposed.")],-1),g=n("code",null,"DbSet<>",-1),v=n("code",null,"DbContext",-1),_=n("code",null,"[InternalUse]",-1),A=n("em",null,"without",-1),F=n("h3",{id:"standard-crud-endpoints",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#standard-crud-endpoints","aria-hidden":"true"},"#"),s(" Standard CRUD Endpoints")],-1),w=n("code",null,"/get",-1),B=n("code",null,"/list",-1),x=n("code",null,"/count",-1),I=n("code",null,"/save",-1),q=n("code",null,"/bulkSave",-1),k=n("code",null,"/delete",-1),S=n("p",null,"The default behavior is that all endpoints require an authenticated user (anonymous users are rejected).",-1),R={href:"https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/actions",target:"_blank",rel:"noopener noreferrer"},T=n("code",null,"[Read]",-1),P=n("em",null,[n("strong",null,"does not")],-1),M=t(`
    EndpointsGoverning Attributes

    /get, /list, /count, /bulkSave

    [ReadAttribute]
    +

    Note: the root model for a bulk save operation requires read permission. All other entities affected by the bulk save operation require their respective attribute for Create/Edit/Delete.

    /save

    [CreateAttribute] // Affects saves of new entities
    +[EditAttribute]   // Affects saves of existing entities
    +

    /delete

    [DeleteAttribute]
    +

    Here are some examples of applying security attributes to an entity class. If a particular action doesn't need to be restricted, you can omit that attribute, but this example shows usages of all four:

    // Allow read access by unauthenticated, anonymous users:
    +[Read(SecurityPermissionLevels.AllowAll)]
    +// Allow creation of new entities by the Admin and HR roles (params string[] style):
    +[Create("Admin", "HR")]
    +// Allow editing of existing Employee entities by users with the Admin or HR roles (CSV style):
    +[Edit("Admin,HR")]
    +// Prohibit deletion of Employee entities
    +[Delete(SecurityPermissionLevels.DenyAll)]
    +public class Employee 
    +{
    +    public int EmployeeId { get; set; }
    +}
    +

    Custom Methods and Services

    `,4),H=t(`

    The default behavior is that all endpoints require an authenticated user (anonymous users are rejected).

    For example:

    public class Employee 
    +{
    +    public int EmployeeId { get; set; }
    +
    +    [Coalesce, Execute("Payroll,HR")]
    +    public void GiveRaise(int centsPerHour) {
    +        // Only Payroll and HR users can call this method
    +    }
    +
    +    [Coalesce, Execute(SecurityPermissionLevels.AllowAll)]
    +    public void SendMessage(string message) {
    +        // Anyone (even anonymous, unauthenticated users) can call this method.
    +    }
    +}
    +

    Property/Column Security

    Internal Properties

    `,5),N=t(`

    The properties in the following example are hidden entirely from all Coalesce functionality and generated APIs:

    using IntelliTect.Coalesce.DataAnnotations;
    +public class Employee 
    +{
    +  // InternalUseAttribute hides anything from Coalesce.
    +  [InternalUse]
    +  public string Name { get; set; }
    +
    +  // Non-public C# access modifiers will hide properties from Coalesce:
    +  internal decimal Salary { get; set; }
    +
    +  // Property's type is [InternalUse], so properties using that type are also internal.
    +  public Department Department { get; set; }
    +}
    +
    +[InternalUse]
    +public class Department
    +{
    +  // All properties on an [InternalUse] type are non-exposed,
    +  // since the parent type is not exposed.
    +  public string Name { get; set; }
    +}
    +

    Attributes

    `,3),U=t(`

    Read-Only Properties

    A property in Coalesce can be made read-only in any of the following ways:

    using IntelliTect.Coalesce.DataAnnotations;
    +using System.ComponentModel;
    +public class Employee 
    +{
    +  // A property with a [Read] attribute but no [Edit] attribute is read-only:
    +  [Read]
    +  public string Name { get; set; }
    +
    +  // Payroll users and HR users can read this property. Nobody can edit it:
    +  [Read("Payroll,HR")]
    +  public decimal Salary { get; set; }
    +
    +  // Using System.ComponentModel.ReadOnlyAttribute:
    +  [ReadOnly(true)]
    +  public DateTime BirthDate { get; set; }
    +
    +  // Non-public setter:
    +  public DateTime StartDate { get; internal set; }
    +
    +  // Edits denied:
    +  [Edit(SecurityPermissionLevels.DenyAll)]
    +  public string EmployeeNumber { get; set; }
    +}
    +

    Read/Write Properties

    Reading and writing a property in Coalesce can be restricted by roles:

    using IntelliTect.Coalesce.DataAnnotations;
    +public class Employee 
    +{
    +  // A property with no attributes is readable and writable without restriction
    +  public string Name { get; set; }
    +
    +  // When a [Read] and [Edit] attributes are both present,
    +  // the read roles are required for edits in addition to any edit roles.
    +  // Property is only readable by Payroll & HR,
    +  // and is also only editable by Payroll & HR.
    +  [Read("Payroll,HR"), Edit]
    +  public DateTime BirthDate { get; set; }
    +
    +  // Property is readable by Payroll and HR, and editable only by Payroll.
    +  [Read("Payroll", "HR"), Edit("Payroll")]
    +  public decimal Salary { get; set; }
    +
    +  // Property is readable by Payroll, and editable only by a user who is both Payroll AND HR.
    +  [Read("Payroll"), Edit("HR")]
    +  public DateTime StartDate { get; set; }
    +
    +  // Init-only properties on entities can only be set by the first /save of the entity.
    +  public string EmployeeNumber { get; init; }
    +}
    +

    A few of the examples above point out that when a property is restricted for reading by roles, those roles are also required when editing that property. This is because it usually doesn't make sense for a user to change a value when they have no way of knowing what the original value was. If you have a situation where a property should be editable without knowing the original value, use a custom method on the model to accept and set the new value.

    Row-level Security

    Data Sources

    `,9),Q=n("code",null,"/get",-1),O=n("code",null,"/list",-1),V=n("code",null,"/count",-1),G=n("code",null,"/save",-1),j=n("code",null,"/bulkSave",-1),L=n("code",null,"/delete",-1),W=n("code",null,"GetQuery",-1),z=n("code",null,"[Read(NoAutoInclude = true)]",-1),Y=t('

    There are a few different techniques that you can use to apply filtering in a data source, each one working for a specific use case. The example below includes an example of each technique.

    Query Predicates

    The Query Predicates technique involves applying a .Where() predicate to your query to filter the root entities that are returned by the query using some database-executed logic. This is a form of row-level security and can be used to only include a record based on the values of that record in the database.

    Conditional Includes

    The Conditional Includes technique involves conditionally appending .Include() calls to your query only when some server-executed criteria is met. Usually this involves checking the roles of a user and only including a navigation property if the user is in the requisite role. This technique cannot be used with database-executed logic and is therefore behaves more like table-level security than row-level security.

    Filtered Includes

    ',6),K=n("strong",null,"Filtered Includes",-1),$={href:"https://learn.microsoft.com/en-us/ef/core/querying/related-data/eager#filtered-include",target:"_blank",rel:"noopener noreferrer"},X=n("strong",null,"cannot",-1),J=n("em",null,"reference",-1),Z={href:"https://github.com/dotnet/efcore/issues/24422",target:"_blank",rel:"noopener noreferrer"},ss=n("a",{href:"#transform-results"},"transform results",-1),ns=n("a",{href:"#ef-global-query-filters"},"global query filters",-1),as=t(`

    A complex example using all three of the above techniques:

    public class Employee 
    +{
    +  public int EmployeeId { get; set; }
    +  public bool IsIntern { get; set; }
    +  public List<DepartmentMember> DepartmentMembers { get; set; }
    +
    +  // Override the default data source for Employee with a custom one:
    +  [DefaultDataSource]
    +  public class DefaultSource : StandardDataSource<Employee, AppDbContext>
    +  {
    +    public DefaultSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Employee> GetQuery(IDataSourceParameters parameters) {
    +      IQueryable<Employee> query = Db.Employees;
    +
    +      // TECHNIQUE: Conditional Includes - subset child objects using server-executed logic:
    +      if (User.IsInRole("HR")) {
    +        // HR can see everything. Return early so they are not subjected to the other filters:
    +        return query.Include(e => e.DepartmentMembers).ThenInclude(dm => dm.Department);
    +      }
    +
    +      // TECHNIQUE: Query Predicates - subset root objects using database-executed logic:
    +      int employeeId = User.GetEmployeeId();
    +      query = query.Where(e => 
    +          // Anyone can see interns
    +          e.IsIntern ||
    +          // Otherwise, a user can only see employees in their own departments:
    +          e.DepartmentMembers.Any(dm => dm.Department.DepartmentMembers.Any(u => u.EmployeeId == employeeId))
    +        );
    +
    +      // TECHNIQUE: EF Core Filtered Includes - subset collections using database-executed logic.
    +      // Include the departments of employees, but only those that the current user is a member of.
    +      query = query.Include(e => e.DepartmentMembers
    +        .Where(dm => dm.Department.DepartmentMembers.Any(u => u.EmployeeId == employeeId)))
    +        .ThenInclude(dm => dm.Department);
    +      
    +      return query;
    +    }
    +  }
    +}
    +
    +public class Department 
    +{
    +  public int DepartmentId { get; set; }
    +  public string Name { get; set; }
    +  public List<DepartmentMember> DepartmentMembers { get; set; }
    +
    +  // Override the default data source for Department with a custom one:
    +  [DefaultDataSource]
    +  public class DefaultSource : StandardDataSource<Department, AppDbContext>
    +  {
    +    public DefaultSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Department> GetQuery(IDataSourceParameters parameters) {
    +      IQueryable<Department> query = Db.Departments
    +        .Include(e => e.DepartmentMembers).ThenInclude(dm => dm.Employee);
    +
    +      if (!User.IsInRole("HR")) 
    +      {
    +        // Non-HR users can only see their own departments:
    +        query = query.Where(d => d.DepartmentMembers.Any(dm => dm.EmployeeId == User.GetEmployeeId()));
    +      }
    +
    +      return query;
    +    }
    +  }
    +}
    +
    +// Only HR can directly read or modify DepartmentMember records.
    +[Read("HR"), Create("HR"), Edit("HR"), Delete("HR")]
    +public class DepartmentMember 
    +{
    +  public int Id { get; set; }
    +
    +  public int DepartmentId { get; set; }
    +  public Department Department { get; set; }
    +  public int EmployeeId { get; set; }
    +  public Employee Employee { get; set; }
    +}
    +
    +

    Transform Results

    `,3),es=n("code",null,"GetQuery",-1),ls=n("code",null,"TransformResults",-1),os=n("code",null,"/get",-1),ps=n("code",null,"/list",-1),ts=n("code",null,"/save",-1),rs=n("code",null,"/bulkSave",-1),Ds=n("code",null,"/delete",-1),cs=n("code",null,"TransformResults",-1),is=n("a",{href:"#filtered-includes"},"filtered includes",-1),ys=n("em",null,"reference",-1),ds={href:"https://github.com/dotnet/efcore/issues/24422",target:"_blank",rel:"noopener noreferrer"},us=n("code",null,"TransformResults",-1),Cs={href:"https://learn.microsoft.com/en-us/ef/core/querying/related-data/explicit#explicit-loading",target:"_blank",rel:"noopener noreferrer"},hs=n("code",null,".IncludedSeparately()",-1),ms=n("code",null,"GetQuery",-1),bs=t(`
    public class Employee 
    +{
    +  public int EmployeeId { get; set; }
    +  public int ManagerId { get; set; }
    +  public Employee Manager { get; set; }
    +
    +  [DefaultDataSource]
    +  public class DefaultSource : StandardDataSource<Employee, AppDbContext>
    +  {
    +    public DefaultSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Employee> GetQuery(IDataSourceParameters parameters) 
    +      // Use IncludedSeparately to instruct Coalesce that we're going to 
    +      // manually populate the Manager, and that it should be mapped to the result DTOs
    +      // despite not being eagerly loaded with EF's .Include() method.
    +      => Db.Employees.IncludedSeparately(e => e.Manager);
    +
    +    public override async Task TransformResultsAsync(
    +      IReadOnlyList<Employee> results,
    +      IDataSourceParameters parameters
    +    )
    +    {
    +      foreach (var employee in results)
    +      {
    +        // Only load the employee's manager if the current logged in user is that manager.
    +        if (employee.ManagerId == User.GetEmployeeId() && employee.Manager is null) {
    +          await Db.Employees.Where(e => e.EmployeeId == employee.ManagerId).LoadAsync();
    +        }
    +      }
    +    }
    +  }
    +}
    +

    Alternatively, and indeed preferably, you can often formulate a query that does not use iteration and requires only a single database round-trip:

    public override async Task TransformResultsAsync(
    +  IReadOnlyList<Employee> results,
    +  IDataSourceParameters parameters
    +)
    +{
    +  var managerIds = results.Select(e => e.ManagerId).ToList();
    +  await Db.Employees
    +    .Where(e => managerIds.Contains(e.ManagerId) && e.EmployeeId == User.GetEmployeeId())
    +    .LoadAsync();
    +}
    +
    `,3),Es=n("h3",{id:"ef-global-query-filters",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#ef-global-query-filters","aria-hidden":"true"},"#"),s(" EF Global Query Filters")],-1),fs={href:"https://learn.microsoft.com/en-us/ef/core/querying/filters",target:"_blank",rel:"noopener noreferrer"},gs={href:"https://learn.microsoft.com/en-us/ef/core/querying/filters#accessing-entity-with-query-filter-using-required-navigation",target:"_blank",rel:"noopener noreferrer"},vs=n("a",{href:"#filtered-includes"},"filtered includes",-1),_s={href:"https://learn.microsoft.com/en-us/ef/core/modeling/relationships/glossary",target:"_blank",rel:"noopener noreferrer"},As=n("code",null,".Include()",-1),Fs={href:"https://github.com/dotnet/efcore/issues/24422",target:"_blank",rel:"noopener noreferrer"},ws=t('

    Foreign Key Injection Vulnerabilities

    When a user is saving a model with Coalesce, they can provide values for the model's foreign key properties. When this interaction takes place through a user interface, the user is not likely to produce a foreign key referencing an object that the user is not allowed to view.

    A malicious user, however, is a different story. Imagine a user who is brute-forcing the /save endpoint on one of your entities, enumerating values of a foreign key. The may be trying to leak data through navigation property values returned by the response from the save, or they may be trying to inject their data into an object graph that they do not otherwise have access to.

    If this scenario sounds like a plausible threat vector your application, be sure to perform sufficient validation of incoming foreign keys to ensure that the user is allowed to use a particular foreign key value before saving it to your database.

    Also consider making any required foreign keys that should not change for the lifetime of an entity into init-only properties (i.e. use the init accessor in C# instead of the set accessor). While this does not entirely solve the foreign key injection issue, it eliminates the need to validate that a user is not changing the parent of an object if such an operation is not desirable.

    Server-side Data Validation

    Coalesce, as of version 4, will by default perform server-side validation of incoming data using validation attributes.

    Your database will also enforce any constraints (referential integrity, not null, check constraints, etc.), but errors produced by your database will manifest as exceptions, which are not user-friendly.

    For any custom validation that cannot be implemented by attributes, you must implement that yourself for saves and deletes or custom methods.

    Attribute Validation

    ',10),Bs={href:"https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute",target:"_blank",rel:"noopener noreferrer"},xs=t(`

    In addition to any validation attributes present on your model properties and method parameters, there are some other rules that work similarly to the default validation in ASP.NET Core:

    • The C# 11 required keyword also acts like a RequiredAttribute
    • If C# nullable reference types are enabled, non-nullable reference types are required required.
    • Non-nullable value types are implicitly optional, with the exception of non-nullable foreign keys, which are required.

    To disable this functionality for your entire application, disable the corresponding configuration options on CoalesceOptions. For example, in Startup.cs or Program.cs:

    services.AddCoalesce<AppDbContext>(b => b.Configure(o =>
    +{
    +    // Set either to false to disable:
    +    o.ValidateAttributesForSaves = true;
    +    o.ValidateAttributesForMethods = true;
    +}));
    +

    Each option also has a more granular override:

    ValidateAttributesForSaves

    `,6),Is=n("code",null,"ValidateAttributesForSaves",-1),qs=n("code",null,"/save",-1),ks=n("code",null,"/bulkSave",-1),Ss=n("code",null,"ValidateAttributesForSaves",-1),Rs=n("h4",{id:"validateattributesformethods",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#validateattributesformethods","aria-hidden":"true"},"#"),s(" ValidateAttributesForMethods")],-1),Ts=n("code",null,"ValidateAttributesForMethods",-1),Ps=n("code",null,"ValidateAttributes",-1),Ms=n("h3",{id:"saves-and-deletes",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#saves-and-deletes","aria-hidden":"true"},"#"),s(" Saves and Deletes")],-1),Hs=n("code",null,"/save",-1),Ns=n("code",null,"/bulkSave",-1),Us=n("code",null,"/delete",-1),Qs=n("a",{href:"#attribute-validation"},"attribute based validation",-1),Os=t(`
    public class Employee 
    +{
    +  public int IsCeo { get; set; }
    +  public decimal Salary { get; set; }
    +
    +  [Coalesce]
    +  public class Behaviors : StandardBehaviors<Employee, AppDbContext>
    +  {
    +    public Behaviors(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override ItemResult BeforeSave(SaveKind kind, Employee? oldItem, Employee item)
    +    {
    +      // \`oldItem\` is a shallow copy of entity from the database,
    +      // and \`item\` is the tracked entity with incoming user data applied to it.
    +      if (item.Salary > 1_000_000m && !oldItem.IsCeo) return "Salary is too high.";
    +      return true;
    +    }
    +
    +    public override ItemResult BeforeDelete(Case item)
    +    {
    +      if (item.IsCeo) return "The CEO cannot be fired.";
    +      return true;
    +    }
    +  }
    +}
    +

    Custom Methods and Services

    `,2),Vs=n("a",{href:"#attribute-validation"},"attribute based validation",-1),Gs=n("code",null,"ItemResult",-1),js=t(`
    public class Employee 
    +{
    +  public decimal Salary { get; set; }
    +
    +  [Coalesce]
    +  public ItemResult<decimal> GiveRaise(decimal raiseAmount)
    +  {
    +    if (raiseAmount > 3.5m) return "Raises must be less than $3.50."
    +    Salary += raiseAmount;
    +    return Salary;
    +  }
    +}
    +

    Security Overview Page

    Coalesce provides batteries-included page that you can view to review the effective security rules in place for all the Coalesce-generated code in your project. Add this page to your application by mapping it as a route, either directly on WebHost in .NET 6+, or in UseEndpoints for 3.1+.

    TIP

    If you include the security overview in your production app, you should secure it with an authorization policy like in the example below. Alternatively, only map the endpoint in non-production environments.

    // .NET 6+ Program.cs:
    +app.MapCoalesceSecurityOverview("coalesce-security").RequireAuthorization(
    +    new AuthorizeAttribute { Roles = env.IsDevelopment() ? null : "Admin" }
    +);
    +
    +// .NET Core 3.1+ Startup.cs:
    +app.UseEndpoints(endpoints =>
    +{
    +    endpoints.MapCoalesceSecurityOverview("coalesce-security").RequireAuthorization(
    +        new AuthorizeAttribute { Roles = env.IsDevelopment() ? null : "Admin" }
    +    );
    +});
    +

    Example of the contents of the security overview page:

    Testing Your Security

    If your application has complex security requirements and/or sensitive data that needs to be protected, you are encouraged to invest time into creating a set of automated tests to ensure that it is working how you expect.

    ',8),Ls={href:"https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests",target:"_blank",rel:"noopener noreferrer"},Ws={href:"https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests#customize-webapplicationfactory",target:"_blank",rel:"noopener noreferrer"},zs={href:"https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-7.0#mock-authentication",target:"_blank",rel:"noopener noreferrer"},Ys=n("em",null,"authorization",-1),Ks=n("em",null,"authentication",-1);function $s(Xs,Js){const o=r("router-link"),l=r("RouterLink"),p=r("ExternalLinkIcon");return c(),i("div",null,[u,C,n("nav",h,[n("ul",null,[n("li",null,[a(o,{to:"#endpoint-security"},{default:e(()=>[s("Endpoint Security")]),_:1}),n("ul",null,[n("li",null,[a(o,{to:"#standard-crud-endpoints"},{default:e(()=>[s("Standard CRUD Endpoints")]),_:1})]),n("li",null,[a(o,{to:"#custom-methods-and-services"},{default:e(()=>[s("Custom Methods and Services")]),_:1})])])]),n("li",null,[a(o,{to:"#property-column-security"},{default:e(()=>[s("Property/Column Security")]),_:1}),n("ul",null,[n("li",null,[a(o,{to:"#internal-properties"},{default:e(()=>[s("Internal Properties")]),_:1})]),n("li",null,[a(o,{to:"#attributes"},{default:e(()=>[s("Attributes")]),_:1})]),n("li",null,[a(o,{to:"#read-only-properties"},{default:e(()=>[s("Read-Only Properties")]),_:1})]),n("li",null,[a(o,{to:"#read-write-properties"},{default:e(()=>[s("Read/Write Properties")]),_:1})])])]),n("li",null,[a(o,{to:"#row-level-security"},{default:e(()=>[s("Row-level Security")]),_:1}),n("ul",null,[n("li",null,[a(o,{to:"#data-sources"},{default:e(()=>[s("Data Sources")]),_:1})]),n("li",null,[a(o,{to:"#ef-global-query-filters"},{default:e(()=>[s("EF Global Query Filters")]),_:1})]),n("li",null,[a(o,{to:"#foreign-key-injection-vulnerabilities"},{default:e(()=>[s("Foreign Key Injection Vulnerabilities")]),_:1})])])]),n("li",null,[a(o,{to:"#server-side-data-validation"},{default:e(()=>[s("Server-side Data Validation")]),_:1}),n("ul",null,[n("li",null,[a(o,{to:"#attribute-validation"},{default:e(()=>[s("Attribute Validation")]),_:1})]),n("li",null,[a(o,{to:"#saves-and-deletes"},{default:e(()=>[s("Saves and Deletes")]),_:1})]),n("li",null,[a(o,{to:"#custom-methods-and-services-1"},{default:e(()=>[s("Custom Methods and Services")]),_:1})])])]),n("li",null,[a(o,{to:"#security-overview-page"},{default:e(()=>[s("Security Overview Page")]),_:1})]),n("li",null,[a(o,{to:"#testing-your-security"},{default:e(()=>[s("Testing Your Security")]),_:1})])])]),m,n("p",null,[s("Coalesce generates API endpoints by traversing your data model's classes, starting from types annotated with "),b,s(". This usually includes your "),E,s(" class, as well as any "),a(l,{to:"/modeling/model-types/services.html"},{default:e(()=>[s("Service")]),_:1}),s(" classes or interfaces.")]),f,n("p",null,[g,s(" properties on your "),v,s(" class can also be annotated with "),_,s(", causing that type to be treated by Coalesce like an "),a(l,{to:"/modeling/model-types/external-types.html"},{default:e(()=>[s("External Type")]),_:1}),s(" rather than an "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[s("Entity")]),_:1}),s(", once again preventing generation of API endpoints but "),A,s(" preventing properties of that type from being exposed.")]),F,n("p",null,[s("For each of your "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[s("Entities")]),_:1}),s(" and "),a(l,{to:"/modeling/model-types/dtos.html"},{default:e(()=>[s("Custom DTOs")]),_:1}),s(", Coalesce generates a set of CRUD API endpoints ("),w,s(", "),B,s(", "),x,s(", "),I,s(", "),q,s(", and "),k,s(").")]),S,n("p",null,[s("These endpoints can be secured by placing any or all of the "),a(l,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:e(()=>[s("[Read], [Create], [Edit], and [Delete] attributes")]),_:1}),s(" on the the class. Each attribute can specify required roles for that action, or open that action to anonymous, unauthenticated users, or disable the endpoint entirely.")]),n("p",null,[s("This security is applied to the generated "),n("a",R,[s("controllers"),a(p)]),s(". The "),T,s(" attribute on a class "),P,s(" affect instances of that class when those instances are present as child properties of other types, since in those scenarios the data will be coming from a different endpoint on a different controller.")]),M,n("p",null,[s("To secure the endpoints generated for your "),a(l,{to:"/modeling/model-components/methods.html"},{default:e(()=>[s("Custom Methods")]),_:1}),s(" and "),a(l,{to:"/modeling/model-types/services.html"},{default:e(()=>[s("Services")]),_:1}),s(", the "),a(l,{to:"/modeling/model-components/attributes/execute.html"},{default:e(()=>[s("[Execute] attribute")]),_:1}),s(" can be used to specify a set of required roles for that endpoint, or to open that endpoint to anonymous users.")]),H,n("p",null,[s("Properties can be hidden from Coalesce entirely, either with the "),a(l,{to:"/modeling/model-components/attributes/internal-use.html"},{default:e(()=>[s("[InternalUse]")]),_:1}),s(" attribute or non-public C# access modifiers.")]),N,n("p",null,[s("The "),a(l,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:e(()=>[s("[Read] and [Edit] attributes")]),_:1}),s(" can be placed on the properties on your "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[s("Entities")]),_:1}),s(" and "),a(l,{to:"/modeling/model-types/external-types.html"},{default:e(()=>[s("External Types")]),_:1}),s(" to apply role-based restrictions to the usage of that property.")]),n("p",null,[s("This security is primarily executed and enforced by the mapping that occurs in the "),a(l,{to:"/stacks/agnostic/dtos.html"},{default:e(()=>[s("generated DTOs")]),_:1}),s(". It is also checked by the "),a(l,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:e(()=>[s("Standard Data Source")]),_:1}),s(" to prevent sorting, searching, and filtering by properties that a user is not permitted to read.")]),U,n("p",null,[s("In Coalesce, "),a(l,{to:"/modeling/model-components/data-sources.html"},{default:e(()=>[s("Data Sources")]),_:1}),s(" are the mechanism that you can extend to implement row-level security on your "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[s("Entities")]),_:1}),s(" and "),a(l,{to:"/modeling/model-types/dtos.html"},{default:e(()=>[s("Custom DTOs")]),_:1}),s(".")]),n("p",null,[s("Data Sources are used when fetching results for "),Q,s(", "),O,s(", and "),V,s(" endpoints, and when fetching the target or result of a "),G,s(", "),j,s(", or "),L,s(", and when fetching the invocation target of an "),a(l,{to:"/modeling/model-components/methods.html#instance-methods"},{default:e(()=>[s("Instance Method")]),_:1}),s(".")]),n("p",null,[s("By default, your entities will be fetched using the "),a(l,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:e(()=>[s("Standard Data Source")]),_:1}),s(", but you can declare a custom default data source for each of your entities to override this default functionality. The default functionality here includes the "),a(l,{to:"/modeling/model-components/data-sources.html#default-loading-behavior"},{default:e(()=>[s("default loading behavior")]),_:1}),s(", a feature where the Standard Data Source automatically includes the immediate relationships of requested entities. This can be suppressed by overriding the "),W,s(" method on your custom data source and not calling the base method, or by placing "),z,s(" on classes or navigation properties that you do not want automatically included.")]),n("p",null,[s("For most use cases, all your security rules will be implemented in the "),a(l,{to:"/modeling/model-components/data-sources.html#member-getquery"},{default:e(()=>[s("GetQuery/GetQueryAsync")]),_:1}),s(" method. This is the most foundational method of the data source that all other functions in the data source build upon. Any predicates applied to the query of a type's default data source will affect all of the type's generated API endpoints (except for static custom methods).")]),Y,n("p",null,[s("The "),K,s(" technique involves using "),n("a",$,[s("EF Core filtered includes"),a(p)]),s(" to apply database-executed logic to filter the rows of child collection navigation properties.")]),n("p",null,[s("EF filtered Includes "),X,s(" be used to apply database-executed filters to "),J,s(" navigation properties due to "),n("a",Z,[s("lack of EF support"),a(p)]),s(" - see the sections below on "),ss,s(" and "),ns,s(" for two possible solutions.")]),as,n("p",null,[s("There exists a fourth technique in Data Sources for applying filtered includes: the "),a(l,{to:"/modeling/model-components/data-sources.html#member-transformresults"},{default:e(()=>[s("TransformResultsAsync")]),_:1}),s(" method. Unlike the other techniques above that are performed in the "),es,s(" method and applied at the beginning of the data source query pipeline, "),ls,s(" is applied at the very end of the process against the materialized results. It also only affects the responses from the generated "),os,s(", "),ps,s(", "),ts,s(", "),rs,s(", and "),Ds,s(" endpoints - it has no bearing on the invocation target of "),a(l,{to:"/modeling/model-components/methods.html#instance-methods"},{default:e(()=>[s("instance methods")]),_:1}),s(".")]),n("p",null,[s("The primary purpose of "),cs,s(" is to conditionally load navigation properties. This was very useful before EF Core introduced native "),is,s(" for collection navigation properties, and is still useful for applying filtered includes to "),ys,s(" navigation properties since EF "),n("a",ds,[s("does not support this"),a(p)]),s(". It can also be used for any kind of filtered includes if native EF filtered includes get translated into poorly-performant SQL, or it can be used to populate "),a(l,{to:"/modeling/model-types/external-types.html"},{default:e(()=>[s("external type")]),_:1}),s(" or other non-database-mapped properties on your entities.")]),n("p",null,[s("The general technique for using "),us,s(" involves using "),n("a",Cs,[s("EF Core Explicit Loading"),a(p)]),s(" to attach additional navigation properties to the result set, and then using Coalesce's "),hs,s(" method in the data source's "),ms,s(" so that Coalesce can still build the correct "),a(l,{to:"/concepts/include-tree.html"},{default:e(()=>[s("Include Tree")]),_:1}),s(" to shape the serialization of your results.")]),bs,n("p",null,[s("For a more complete explanation of everything you can do with data sources, see the full "),a(l,{to:"/modeling/model-components/data-sources.html"},{default:e(()=>[s("Data Sources")]),_:1}),s(" documentation page.")]),Es,n("p",null,[s("Since Coalesce's data access layer is built on top of Entity Framework, you can also use "),n("a",fs,[s("Entity Framework's Global Query Filters"),a(p)]),s(" feature to apply row-level security.")]),n("p",null,[s("This approach is less flexible than custom Coalesce data sources and has other "),n("a",gs,[s("drawbacks"),a(p)]),s(" as well, but on the other hand it has more absolute authority, is less susceptible to issues like inadvertently returning data through unfiltered navigation properties, and can sometimes require less work to implement than individual data sources.")]),n("p",null,[s("Global Query Filters are also the only way to implement database-executed "),vs,s(" of "),n("a",_s,[s("reference navigation properties"),a(p)]),s(", as there is no version of "),As,s(" for reference navigation properties that allows a database-executed predicate to be applied. See "),n("a",Fs,[s("this open issue"),a(p)]),s(" on EF Core.")]),ws,n("p",null,[s("Historically, Coalesce did not provide any automatic, attribute-based validation of incoming data. As of Coalesce 4.0, automatic server side validation using "),n("a",Bs,[s("ValidationAttribute"),a(p)]),s("-derived attributes on your models is enabled by default.")]),xs,n("p",null,[s("Enabling "),Is,s(" causes the "),a(l,{to:"/modeling/model-components/behaviors.html#standard-behaviors"},{default:e(()=>[s("Standard Behaviors")]),_:1}),s(" to perform validation of validation attributes during "),qs,s(" or "),ks,s(" calls, preventing a save when validation fails.")]),n("p",null,[s("This can be overridden per type or even per request by setting the "),Ss,s(" property on a "),a(l,{to:"/modeling/model-components/behaviors.html#defining-behaviors"},{default:e(()=>[s("custom Behaviors")]),_:1}),s(" instance.")]),Rs,n("p",null,[s("Enabling "),a(l,{to:"/modeling/model-components/attributes/execute.html#member-validateattributes"},{default:e(()=>[Ts]),_:1}),s(" causes the generated controllers for "),a(l,{to:"/modeling/model-components/methods.html"},{default:e(()=>[s("custom methods")]),_:1}),s(" to perform validation of incoming parameters. Validation attributes may be placed on method parameters, and validation will also be performed against the members of any complex type parameters.")]),n("p",null,[s("This can be overridden per method by setting the "),Ps,s(" property on "),a(l,{to:"/modeling/model-components/attributes/execute.html"},{default:e(()=>[s("ExecuteAttribute")]),_:1}),s(" for the method.")]),Ms,n("p",null,[s("Validation of "),Hs,s(", "),Ns,s(", and "),Us,s(" actions against "),a(l,{to:"/modeling/model-types/entities.html"},{default:e(()=>[s("Entities")]),_:1}),s(" and "),a(l,{to:"/modeling/model-types/dtos.html"},{default:e(()=>[s("Custom DTOs")]),_:1}),s(" are performed by the "),a(l,{to:"/modeling/model-components/behaviors.html"},{default:e(()=>[s("Behaviors")]),_:1}),s(" for the type. Automatic "),Qs,s(" can be used (saves only), or Behaviors can be overridden to perform validation and other customization of the save and delete process, as in the following example:")]),Os,n("p",null,[s("For "),a(l,{to:"/modeling/model-components/methods.html"},{default:e(()=>[s("Custom Methods")]),_:1}),s(" and "),a(l,{to:"/modeling/model-types/services.html"},{default:e(()=>[s("Services")]),_:1}),s(", you can perform your own custom validation and return errors when validation fails. You can also use "),Vs,s(". Custom methods that need to return errors to the client are recommended to wrap their return type in an "),Gs,s(", allowing errors to be received and handled elegantly by your Coalesce Typescript code.")]),js,n("p",null,[s("The most comprehensive way to do this is to build a suite of integration tests using "),n("a",Ls,[s("Microsoft's in-memory test server infrastructure"),a(p)]),s(". Follow Microsoft's documentation to set up a test project, and then write tests against your API endpoints. You will want to "),n("a",Ws,[s("substitute your Entity Framework database provider"),a(p)]),s(" with an in-memory Sqlite instance, and add a "),n("a",zs,[s("mock authentication handler"),a(p)]),s(" to simulate authentication (we're mainly focused on testing "),Ys,s(", not "),Ks,s(").")])])}const sn=D(d,[["render",$s],["__file","security.html.vue"]]);export{sn as default}; diff --git a/assets/select-filter.html.0de35596.js b/assets/select-filter.html.0de35596.js new file mode 100644 index 000000000..13fffed89 --- /dev/null +++ b/assets/select-filter.html.0de35596.js @@ -0,0 +1,21 @@ +import{_ as l,y as o,z as p,Q as n,a5 as t,X as s,B as e,P as r}from"./framework.fe9a73df.js";const c={},D=t(`

    [SelectFilter]

    WARNING

    This attribute only affects the generated Knockout HTML views - it does not enforce any relational rules in your data.

    This attribute also currently has no effect against the Vue stack.

    Specify a property to restrict dropdown menus by. Values presented will be only those where the value of the foreign property matches the value of the local property.

    The local property name defaults to the same value of the foreign property.

    Additionally, in place of a LocalPropertyName to check against, you may instead specify a static value using StaticPropertyValue to filter by a constant.

    Example Usage

    In this example, a dropdown for EmployeeRank created using @Knockout.SelectForObject in cshtml files will only present possible values of EmployeeRank which are valid for the EmployeeType of the Employee.

    public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +    public int EmployeeTypeId { get; set; }
    +    public EmployeeType EmployeeType { get; set; }
    +    public int EmployeeRankId { get; set; }
    +
    +    [SelectFilter(ForeignPropertyName = nameof(EmployeeRank.EmployeeTypeId), LocalPropertyName = nameof(Employee.EmployeeTypeId))]
    +    public EmployeeRank EmployeeRank { get; set; }
    +}
    +
    +public class EmployeeRank
    +{
    +    public int EmployeeRankId { get; set; }
    +    public int EmployeeTypeId { get; set; }
    +    public EmployeeType EmployeeType { get; set; }
    +}
    +
    <div>
    +    @(Knockout.SelectForObject<Models.Employee>(e => e.EmployeeRank))
    +</div>
    +

    Properties

    `,10),i=s("p",null,"The name of the property on the foreign object to filter against.",-1),y=s("p",null,"The name of another property belonging to the class in which this attribute is used. The results of select lists will be filtered to match this value.",-1),d=s("p",null,[e("Defaults to the value of "),s("code",null,"ForeignPropertyName"),e(" if not set.")],-1),u=s("p",null,[e("If specified, the "),s("code",null,"LocalPropertyName"),e(" will be resolved from the property by this name that resides on the local object.")],-1),C=s("p",null,"This allows for querying against properties that are one level away from the current object.",-1),m=s("p",null,[e("A constant value that the foreign property will be filtered against. This string must be parsable into the foreign property's type to have any effect. If this is set, "),s("code",null,"LocalPropertyName"),e(" will be ignored.")],-1);function h(v,b){const a=r("Prop");return o(),p("div",null,[D,n(a,{def:"public string ForeignPropertyName { get; set; }"}),i,n(a,{def:"public string LocalPropertyName { get; set; }"}),y,d,n(a,{def:"public string LocalPropertyObjectName { get; set; }"}),u,C,n(a,{def:"public string StaticPropertyValue { get; set; }"}),m])}const E=l(c,[["render",h],["__file","select-filter.html.vue"]]);export{E as default}; diff --git a/assets/select-filter.html.a53f4884.js b/assets/select-filter.html.a53f4884.js new file mode 100644 index 000000000..6fd41d997 --- /dev/null +++ b/assets/select-filter.html.a53f4884.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-3a375f50","path":"/modeling/model-components/attributes/select-filter.html","title":"[SelectFilter]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1677792094000},"filePathRelative":"modeling/model-components/attributes/select-filter.md"}');export{e as data}; diff --git a/assets/services.html.1143f6c9.js b/assets/services.html.1143f6c9.js new file mode 100644 index 000000000..de32d2efd --- /dev/null +++ b/assets/services.html.1143f6c9.js @@ -0,0 +1,30 @@ +import{_ as o,y as p,z as t,X as n,B as s,Q as a,$ as l,a5 as c,P as r}from"./framework.fe9a73df.js";const i={},D=n("h1",{id:"services",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#services","aria-hidden":"true"},"#"),s(" Services")],-1),d=n("p",null,[s("Instead, Coalesce allows you to generate API Controllers and a TypeScript client from a service. A service, in this case, is nothing more than a C# class or an interface with methods on it, annotated with "),n("code",null,"[Coalesce,Service]"),s(". An implementation of this class or interface must be injectable from your application's service container, so a registration in Startup.cs is needed.")],-1),y=n("h2",{id:"generated-code",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#generated-code","aria-hidden":"true"},"#"),s(" Generated Code")],-1),u=n("p",null,"For each external type found in your application's model, Coalesce will generate:",-1),v=n("li",null,"An API controller with endpoints that correspond to the service's instance methods.",-1),h=c(`

    Example Service

    An example of a service might look something like this:

    [Coalesce, Service]
    +public interface IWeatherService
    +{
    +    WeatherData GetWeather(string zipCode);
    +}
    +

    With an implementation:

    public class WeatherService : IWeatherService
    +{
    +    public WeatherService(AppDbContext db)
    +    {
    +        this.db = db;
    +    }
    +
    +    public WeatherData GetWeather(string zipCode)
    +    {
    +        // Assuming some magic HttpGet method that works as follows...
    +        var response = HttpGet("http://www.example.com/api/weather/" + zipCode);
    +        return response.Body.SerializeTo<WeatherData>();
    +    }
    +
    +    public void MethodThatIsNotExposedBecauseItIsNotOnTheExposedInterface() {  }
    +}
    +

    And a registration:

    public class Startup 
    +{
    +    public void ConfigureServices(IServiceCollection services)
    +    {
    +        services.AddCoalesce<AppDbContext>();
    +        services.AddScoped<IWeatherService, WeatherService>();
    +    }
    +}
    +

    While it isn't required that an interface for your service exist - you can generate directly from the implementation, it is highly recommended that an interface be used. Interfaces increase testability and reduce risk of accidentally changing the signature of a published API, among other benefits.

    `,8);function m(C,b){const e=r("RouterLink");return p(),t("div",null,[D,n("p",null,[s("In a Coalesce, you are fairly likely to end up with a need for some API endpoints that aren't closely tied with your regular data model. While you could stick static "),a(e,{to:"/modeling/model-components/methods.html"},{default:l(()=>[s("Methods")]),_:1}),s(" on one of your entities, this solution just leads to a jumbled mess of functionality all over your data model that doesn't belong there.")]),d,n("p",null,[s("The instance methods of these services work just like other custom "),a(e,{to:"/modeling/model-components/methods.html"},{default:l(()=>[s("Methods")]),_:1}),s(" in Coalesce, with one notable distinction: Instance methods don't operate on an instance of a model, but instead on a dependency injected instance of the service.")]),y,u,n("ul",null,[v,n("li",null,[s("A TypeScript client containing the members outlined in "),a(e,{to:"/modeling/model-components/methods.html"},{default:l(()=>[s("Methods")]),_:1}),s(" for invoking these endpoints.")])]),h])}const E=o(i,[["render",m],["__file","services.html.vue"]]);export{E as default}; diff --git a/assets/services.html.c1892de9.js b/assets/services.html.c1892de9.js new file mode 100644 index 000000000..680dfdf93 --- /dev/null +++ b/assets/services.html.c1892de9.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-d699f478","path":"/modeling/model-types/services.html","title":"Services","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Generated Code","slug":"generated-code","link":"#generated-code","children":[]},{"level":2,"title":"Example Service","slug":"example-service","link":"#example-service","children":[]}],"git":{"updatedTime":1663267711000},"filePathRelative":"modeling/model-types/services.md"}');export{e as data}; diff --git a/assets/startup.html.05150e47.js b/assets/startup.html.05150e47.js new file mode 100644 index 000000000..286b665eb --- /dev/null +++ b/assets/startup.html.05150e47.js @@ -0,0 +1 @@ +const t=JSON.parse('{"key":"v-48c2b024","path":"/topics/startup.html","title":"Application Configuration","lang":"en-US","frontmatter":{},"excerpt":"","headers":[],"git":{"updatedTime":1680124405000},"filePathRelative":"topics/startup.md"}');export{t as data}; diff --git a/assets/startup.html.87b1cf60.js b/assets/startup.html.87b1cf60.js new file mode 100644 index 000000000..f88bf734e --- /dev/null +++ b/assets/startup.html.87b1cf60.js @@ -0,0 +1,30 @@ +import{_ as o,y as p,z as t,X as s,B as n,Q as a,$ as l,a5 as c,P as r}from"./framework.fe9a73df.js";const D={},i=c(`

    Application Configuration

    In order for Coalesce to work in your application, you must register the needed services in your Startup.cs file. Doing so is simple:

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddCoalesce<AppDbContext>();
    +    ...
    +}
    +

    This registers all the basic services that Coalesce needs in order to work with your EF DbContext. However, there are many more options available. Here's a more complete invocation of AddCoalesce that takes advantage of many of the options available:

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddCoalesce(builder => builder
    +        .AddContext<AppDbContext>()
    +        .UseDefaultDataSource(typeof(MyDataSource<,>))
    +        .UseDefaultBehaviors(typeof(MyBehaviors<,>))
    +        .UseTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"))
    +        .Configure(o =>
    +        {
    +            o.ValidateAttributesForMethods = true; // note: true is the default
    +            o.ValidateAttributesForSaves = true; // note: true is the default
    +            o.DetailedExceptionMessages = true;
    +            o.ExceptionResponseFactory = ctx =>
    +            {
    +                if (ctx.Exception is FileNotFoundException)
    +                {
    +                    ctx.HttpContext.Response.StatusCode = 404; // Optional - set a specific response code.
    +                    return new IntelliTect.Coalesce.Models.ApiResult(false, "File not found");
    +                }
    +                return null;
    +            };
    +        });
    +    );
    +}
    +

    A summary is as follows:

    .AddContext<AppDbContext>()

    Register services needed by Coalesce to use the specified context. This is done automatically when calling the services.AddCoalesce<AppDbContext>(); overload.

    .UseDefaultDataSource(typeof(MyDataSource<,>))

    `,9),d=s("p",null,[s("code",null,".UseDefaultBehaviors(typeof(MyBehaviors<,>))")],-1),y=s("p",null,[s("code",null,'.UseTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"))')],-1),u=s("p",null,"Specify a static time zone that should be used when Coalesce is performing operations on dates/times that lack timezone information. For example, when a user inputs a search term that contains only a date, Coalesce needs to know what timezone's midnight to use when performing the search.",-1),C=s("p",null,[s("code",null,".UseTimeZone()")],-1),v=s("p",null,"Specify a service implementation to use to resolve the current timezone. This should be a scoped service, and will be automatically registered if it is not already. This allows retrieving timezone information on a per-request basis from HTTP headers, Cookies, or any other source.",-1),m=s("p",null,[s("code",null,".Configure(...)")],-1),h=s("p",null,"Configure additional options for Coalesce runtime behavior. Current options include options for server-side validation, and options for exception handling. See individual members for details.",-1);function b(f,E){const e=r("RouterLink");return p(),t("div",null,[i,s("p",null,[n("Overrides the default data source used, replacing the "),a(e,{to:"/modeling/model-components/data-sources.html#standard-data-source"},{default:l(()=>[n("Standard Data Source")]),_:1}),n(". See "),a(e,{to:"/modeling/model-components/data-sources.html"},{default:l(()=>[n("Data Sources")]),_:1}),n(" for more details.")]),d,s("p",null,[n("Overrides the default behaviors used, replacing the "),a(e,{to:"/modeling/model-components/behaviors.html#standard-behaviors"},{default:l(()=>[n("Standard Behaviors")]),_:1}),n(". See "),a(e,{to:"/modeling/model-components/behaviors.html"},{default:l(()=>[n("Behaviors")]),_:1}),n(" for more details.")]),y,u,C,v,m,h])}const A=o(D,[["render",b],["__file","startup.html.vue"]]);export{A as default}; diff --git a/assets/style.2c165801.css b/assets/style.2c165801.css new file mode 100644 index 000000000..ac8dceffa --- /dev/null +++ b/assets/style.2c165801.css @@ -0,0 +1 @@ +:root{--back-to-top-z-index: 5;--back-to-top-color: #3eaf7c;--back-to-top-color-hover: #71cda3}.back-to-top{cursor:pointer;position:fixed;bottom:2rem;right:2.5rem;width:2rem;height:1.2rem;background-color:var(--back-to-top-color);-webkit-mask:url(/Coalesce/assets/back-to-top.8efcbe56.svg) no-repeat;mask:url(/Coalesce/assets/back-to-top.8efcbe56.svg) no-repeat;z-index:var(--back-to-top-z-index)}.back-to-top:hover{background-color:var(--back-to-top-color-hover)}@media (max-width: 959px){.back-to-top{display:none}}@media print{.back-to-top{display:none}}.back-to-top-enter-active,.back-to-top-leave-active{transition:opacity .3s}.back-to-top-enter-from,.back-to-top-leave-to{opacity:0}:root{--external-link-icon-color: #aaa}.external-link-icon{position:relative;display:inline-block;color:var(--external-link-icon-color);vertical-align:middle;top:-1px}@media print{.external-link-icon{display:none}}.external-link-icon-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--nprogress-color: #29d;--nprogress-z-index: 1031}#nprogress{pointer-events:none}#nprogress .bar{background:var(--nprogress-color);position:fixed;z-index:var(--nprogress-z-index);top:0;left:0;width:100%;height:2px}:root{--c-brand: #3eaf7c;--c-brand-light: #4abf8a;--c-bg: #ffffff;--c-bg-light: #f3f4f5;--c-bg-lighter: #eeeeee;--c-bg-dark: #ebebec;--c-bg-darker: #e6e6e6;--c-bg-navbar: var(--c-bg);--c-bg-sidebar: var(--c-bg);--c-bg-arrow: #cccccc;--c-text: #2c3e50;--c-text-accent: var(--c-brand);--c-text-light: #3a5169;--c-text-lighter: #4e6e8e;--c-text-lightest: #6a8bad;--c-text-quote: #999999;--c-border: #eaecef;--c-border-dark: #dfe2e5;--c-tip: #42b983;--c-tip-bg: var(--c-bg-light);--c-tip-title: var(--c-text);--c-tip-text: var(--c-text);--c-tip-text-accent: var(--c-text-accent);--c-warning: #ffc310;--c-warning-bg: #fffae3;--c-warning-bg-light: #fff3ba;--c-warning-bg-lighter: #fff0b0;--c-warning-border-dark: #f7dc91;--c-warning-details-bg: #fff5ca;--c-warning-title: #f1b300;--c-warning-text: #746000;--c-warning-text-accent: #edb100;--c-warning-text-light: #c1971c;--c-warning-text-quote: #ccab49;--c-danger: #f11e37;--c-danger-bg: #ffe0e0;--c-danger-bg-light: #ffcfde;--c-danger-bg-lighter: #ffc9c9;--c-danger-border-dark: #f1abab;--c-danger-details-bg: #ffd4d4;--c-danger-title: #ed1e2c;--c-danger-text: #660000;--c-danger-text-accent: #bd1a1a;--c-danger-text-light: #b5474d;--c-danger-text-quote: #c15b5b;--c-details-bg: #eeeeee;--c-badge-tip: var(--c-tip);--c-badge-warning: #ecc808;--c-badge-warning-text: var(--c-bg);--c-badge-danger: #dc2626;--c-badge-danger-text: var(--c-bg);--t-color: .3s ease;--t-transform: .3s ease;--code-bg-color: #282c34;--code-hl-bg-color: rgba(0, 0, 0, .66);--code-ln-color: #9e9e9e;--code-ln-wrapper-width: 3.5rem;--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;--font-family-code: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;--navbar-height: 3.6rem;--navbar-padding-v: .7rem;--navbar-padding-h: 1.5rem;--sidebar-width: 20rem;--sidebar-width-mobile: calc(var(--sidebar-width) * .82);--content-width: 740px;--homepage-width: 960px}.back-to-top{--back-to-top-color: var(--c-brand);--back-to-top-color-hover: var(--c-brand-light)}.DocSearch{--docsearch-primary-color: var(--c-brand);--docsearch-text-color: var(--c-text);--docsearch-highlight-color: var(--c-brand);--docsearch-muted-color: var(--c-text-quote);--docsearch-container-background: rgba(9, 10, 17, .8);--docsearch-modal-background: var(--c-bg-light);--docsearch-searchbox-background: var(--c-bg-lighter);--docsearch-searchbox-focus-background: var(--c-bg);--docsearch-searchbox-shadow: inset 0 0 0 2px var(--c-brand);--docsearch-hit-color: var(--c-text-light);--docsearch-hit-active-color: var(--c-bg);--docsearch-hit-background: var(--c-bg);--docsearch-hit-shadow: 0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background: var(--c-bg)}.external-link-icon{--external-link-icon-color: var(--c-text-quote)}.medium-zoom-overlay{--medium-zoom-bg-color: var(--c-bg)}#nprogress{--nprogress-color: var(--c-brand)}.pwa-popup{--pwa-popup-text-color: var(--c-text);--pwa-popup-bg-color: var(--c-bg);--pwa-popup-border-color: var(--c-brand);--pwa-popup-shadow: 0 4px 16px var(--c-brand);--pwa-popup-btn-text-color: var(--c-bg);--pwa-popup-btn-bg-color: var(--c-brand);--pwa-popup-btn-hover-bg-color: var(--c-brand-light)}.search-box{--search-bg-color: var(--c-bg);--search-accent-color: var(--c-brand);--search-text-color: var(--c-text);--search-border-color: var(--c-border);--search-item-text-color: var(--c-text-lighter);--search-item-focus-bg-color: var(--c-bg-light)}html.dark{--c-brand: #3aa675;--c-brand-light: #349469;--c-bg: #22272e;--c-bg-light: #2b313a;--c-bg-lighter: #262c34;--c-bg-dark: #343b44;--c-bg-darker: #37404c;--c-text: #adbac7;--c-text-light: #96a7b7;--c-text-lighter: #8b9eb0;--c-text-lightest: #8094a8;--c-border: #3e4c5a;--c-border-dark: #34404c;--c-tip: #318a62;--c-warning: #e0ad15;--c-warning-bg: #2d2f2d;--c-warning-bg-light: #423e2a;--c-warning-bg-lighter: #44442f;--c-warning-border-dark: #957c35;--c-warning-details-bg: #39392d;--c-warning-title: #fdca31;--c-warning-text: #d8d96d;--c-warning-text-accent: #ffbf00;--c-warning-text-light: #ddb84b;--c-warning-text-quote: #ccab49;--c-danger: #fc1e38;--c-danger-bg: #39232c;--c-danger-bg-light: #4b2b35;--c-danger-bg-lighter: #553040;--c-danger-border-dark: #a25151;--c-danger-details-bg: #482936;--c-danger-title: #fc2d3b;--c-danger-text: #ea9ca0;--c-danger-text-accent: #fd3636;--c-danger-text-light: #d9777c;--c-danger-text-quote: #d56b6b;--c-details-bg: #323843;--c-badge-warning: var(--c-warning);--c-badge-warning-text: #3c2e05;--c-badge-danger: var(--c-danger);--c-badge-danger-text: #401416;--code-hl-bg-color: #363b46}html.dark .DocSearch{--docsearch-logo-color: var(--c-text);--docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;--docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d, 0 2px 2px 0 rgba(3, 4, 9, .3);--docsearch-key-gradient: linear-gradient(-225deg, #444950, #1c1e21);--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, .5), 0 -4px 8px 0 rgba(0, 0, 0, .2)}html,body{padding:0;margin:0;background-color:var(--c-bg);transition:background-color var(--t-color)}html.dark{color-scheme:dark}html{font-size:16px}body{font-family:var(--font-family);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:1rem;color:var(--c-text)}a{font-weight:500;color:var(--c-text-accent);text-decoration:none;overflow-wrap:break-word}p a code{font-weight:400;color:var(--c-text-accent)}kbd{font-family:var(--font-family-code);color:var(--c-text);background:var(--c-bg-lighter);border:solid .15rem var(--c-border-dark);border-bottom:solid .25rem var(--c-border-dark);border-radius:.15rem;padding:0 .15em}code{font-family:var(--font-family-code);color:var(--c-text-lighter);padding:.25rem .5rem;margin:0;font-size:.85em;background-color:var(--c-bg-light);border-radius:3px;overflow-wrap:break-word;transition:background-color var(--t-color)}blockquote{font-size:1rem;color:var(--c-text-quote);border-left:.2rem solid var(--c-border-dark);margin:1rem 0;padding:.25rem 0 .25rem 1rem}blockquote>p{margin:0}ul,ol{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25}h1:focus-visible,h2:focus-visible,h3:focus-visible,h4:focus-visible,h5:focus-visible,h6:focus-visible{outline:none}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color)}h3{font-size:1.35rem}h4{font-size:1.15rem}h5{font-size:1.05rem}h6{font-size:1rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media print{a.header-anchor{display:none}}a.header-anchor:hover{text-decoration:none}a.header-anchor:focus-visible{opacity:1}@media print{a[href^="http://"]:after,a[href^="https://"]:after{content:" (" attr(href) ") "}}p,ul,ol{line-height:1.7}hr{border:0;border-top:1px solid var(--c-border)}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto;transition:border-color var(--t-color)}tr{border-top:1px solid var(--c-border-dark);transition:border-color var(--t-color)}tr:nth-child(2n){background-color:var(--c-bg-light);transition:background-color var(--t-color)}tr:nth-child(2n) code{background-color:var(--c-bg-dark)}th,td{padding:.6em 1em;border:1px solid var(--c-border-dark);transition:border-color var(--t-color)}.arrow{display:inline-block;width:0;height:0}.arrow.up{border-left:4px solid transparent;border-right:4px solid transparent;border-bottom:6px solid var(--c-bg-arrow)}.arrow.down{border-left:4px solid transparent;border-right:4px solid transparent;border-top:6px solid var(--c-bg-arrow)}.arrow.right{border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid var(--c-bg-arrow)}.arrow.left{border-top:4px solid transparent;border-bottom:4px solid transparent;border-right:6px solid var(--c-bg-arrow)}.badge{display:inline-block;font-size:14px;font-weight:600;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:var(--c-bg);vertical-align:top;transition:color var(--t-color),background-color var(--t-color)}.badge.tip{background-color:var(--c-badge-tip)}.badge.warning{background-color:var(--c-badge-warning);color:var(--c-badge-warning-text)}.badge.danger{background-color:var(--c-badge-danger);color:var(--c-badge-danger-text)}.badge+.badge{margin-left:5px}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:var(--font-family-code);font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#ec5975}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.4;padding:1.3rem 1.5rem;margin:.85rem 0;border-radius:6px;overflow:auto}.theme-default-content pre code,.theme-default-content pre[class*=language-] code{color:#fff;padding:0;background-color:transparent!important;border-radius:0;overflow-wrap:unset;-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.theme-default-content .line-number{font-family:var(--font-family-code)}div[class*=language-]{position:relative;background-color:var(--code-bg-color);border-radius:6px}div[class*=language-]:before{content:attr(data-ext);position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:var(--code-ln-color)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent!important;position:relative;z-index:1}div[class*=language-] .highlight-lines{-webkit-user-select:none;-moz-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.4}div[class*=language-] .highlight-lines .highlight-line{background-color:var(--code-hl-bg-color)}div[class*=language-]:not(.line-numbers-mode) .line-numbers{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlight-line:before{content:" ";position:absolute;z-index:2;left:0;top:0;display:block;width:var(--code-ln-wrapper-width);height:100%}div[class*=language-].line-numbers-mode pre{margin-left:var(--code-ln-wrapper-width);padding-left:1rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers{position:absolute;top:0;width:var(--code-ln-wrapper-width);text-align:center;color:var(--code-ln-color);padding-top:1.25rem;line-height:1.4;counter-reset:line-number}div[class*=language-].line-numbers-mode .line-numbers .line-number{position:relative;z-index:3;-webkit-user-select:none;-moz-user-select:none;user-select:none;height:1.4em}div[class*=language-].line-numbers-mode .line-numbers .line-number:before{counter-increment:line-number;content:counter(line-number);font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;top:0;left:0;width:var(--code-ln-wrapper-width);height:100%;border-radius:6px 0 0 6px;border-right:1px solid var(--code-hl-bg-color)}@media (max-width: 419px){.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.code-group__nav{margin-top:.85rem;margin-bottom:calc(-1.7rem - 6px);padding-bottom:calc(1.7rem - 6px);padding-left:10px;padding-top:10px;border-top-left-radius:6px;border-top-right-radius:6px;background-color:var(--code-bg-color)}.code-group__ul{margin:auto 0;padding-left:0;display:inline-flex;list-style:none}.code-group__nav-tab{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:#ffffffe6;font-weight:600}.code-group__nav-tab:focus{outline:none}.code-group__nav-tab:focus-visible{outline:1px solid rgba(255,255,255,.9)}.code-group__nav-tab-active{border-bottom:var(--c-brand) 1px solid}@media (max-width: 419px){.code-group__nav{margin-left:-1.5rem;margin-right:-1.5rem;border-radius:0}}.code-group-item{display:none}.code-group-item__active{display:block}.code-group-item>pre{background-color:orange}.custom-container{transition:color var(--t-color),border-color var(--t-color),background-color var(--t-color)}.custom-container .custom-container-title{font-weight:600}.custom-container .custom-container-title:not(:only-child){margin-bottom:-.4rem}.custom-container.tip,.custom-container.warning,.custom-container.danger{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-container.tip{border-color:var(--c-tip);background-color:var(--c-tip-bg);color:var(--c-tip-text)}.custom-container.tip .custom-container-title{color:var(--c-tip-title)}.custom-container.tip a{color:var(--c-tip-text-accent)}.custom-container.tip code{background-color:var(--c-bg-dark)}.custom-container.warning{border-color:var(--c-warning);background-color:var(--c-warning-bg);color:var(--c-warning-text)}.custom-container.warning .custom-container-title{color:var(--c-warning-title)}.custom-container.warning a{color:var(--c-warning-text-accent)}.custom-container.warning blockquote{border-left-color:var(--c-warning-border-dark);color:var(--c-warning-text-quote)}.custom-container.warning code{color:var(--c-warning-text-light);background-color:var(--c-warning-bg-light)}.custom-container.warning details{background-color:var(--c-warning-details-bg)}.custom-container.warning details code{background-color:var(--c-warning-bg-lighter)}.custom-container.warning .external-link-icon{--external-link-icon-color: var(--c-warning-text-quote)}.custom-container.danger{border-color:var(--c-danger);background-color:var(--c-danger-bg);color:var(--c-danger-text)}.custom-container.danger .custom-container-title{color:var(--c-danger-title)}.custom-container.danger a{color:var(--c-danger-text-accent)}.custom-container.danger blockquote{border-left-color:var(--c-danger-border-dark);color:var(--c-danger-text-quote)}.custom-container.danger code{color:var(--c-danger-text-light);background-color:var(--c-danger-bg-light)}.custom-container.danger details{background-color:var(--c-danger-details-bg)}.custom-container.danger details code{background-color:var(--c-danger-bg-lighter)}.custom-container.danger .external-link-icon{--external-link-icon-color: var(--c-danger-text-quote)}.custom-container.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:var(--c-details-bg)}.custom-container.details code{background-color:var(--c-bg-darker)}.custom-container.details h4{margin-top:0}.custom-container.details figure:last-child,.custom-container.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-container.details summary{outline:none;cursor:pointer}.home{padding:var(--navbar-height) 2rem 0;max-width:var(--homepage-width);margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.8rem auto}.home .hero .actions{display:flex;flex-wrap:wrap;gap:1rem;justify-content:center}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:var(--c-text-lightest)}.home .hero .action-button{display:inline-block;font-size:1.2rem;padding:.8rem 1.6rem;border-width:2px;border-style:solid;border-radius:4px;transition:background-color var(--t-color);box-sizing:border-box}.home .hero .action-button.primary{color:var(--c-bg);background-color:var(--c-brand);border-color:var(--c-brand)}.home .hero .action-button.primary:hover{background-color:var(--c-brand-light)}.home .hero .action-button.secondary{color:var(--c-brand);background-color:var(--c-bg);border-color:var(--c-brand)}.home .hero .action-button.secondary:hover{color:var(--c-bg);background-color:var(--c-brand-light)}.home .features{border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:var(--c-text-light)}.home .feature p{color:var(--c-text-lighter)}.home .theme-default-content{padding:0;margin:0}.home .footer{padding:2.5rem;border-top:1px solid var(--c-border);text-align:center;color:var(--c-text-lighter);transition:border-color var(--t-color)}@media (max-width: 719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width: 419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero h1,.home .hero .description,.home .hero .actions{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page{padding-top:var(--navbar-height);padding-left:var(--sidebar-width)}.navbar{position:fixed;z-index:20;top:0;left:0;right:0;height:var(--navbar-height);box-sizing:border-box;border-bottom:1px solid var(--c-border);background-color:var(--c-bg-navbar);transition:background-color var(--t-color),border-color var(--t-color)}.sidebar{font-size:16px;width:var(--sidebar-width);position:fixed;z-index:10;margin:0;top:var(--navbar-height);left:0;bottom:0;box-sizing:border-box;border-right:1px solid var(--c-border);overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--c-brand) var(--c-border);background-color:var(--c-bg-sidebar);transition:transform var(--t-transform),background-color var(--t-color),border-color var(--t-color)}.sidebar::-webkit-scrollbar{width:7px}.sidebar::-webkit-scrollbar-track{background-color:var(--c-border)}.sidebar::-webkit-scrollbar-thumb{background-color:var(--c-brand)}.sidebar-mask{position:fixed;z-index:9;top:0;left:0;width:100vw;height:100vh;display:none}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1){transform:rotate(45deg) translate3d(5.5px,5.5px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(2){transform:scale3d(0,1,1)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform:rotate(-45deg) translate3d(6px,-6px,0)}.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(1),.theme-container.sidebar-open .navbar>.toggle-sidebar-button .icon span:nth-child(3){transform-origin:center}.theme-container.no-navbar .theme-default-content h1,.theme-container.no-navbar .theme-default-content h2,.theme-container.no-navbar .theme-default-content h3,.theme-container.no-navbar .theme-default-content h4,.theme-container.no-navbar .theme-default-content h5,.theme-container.no-navbar .theme-default-content h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .page{padding-top:0}.theme-container.no-navbar .sidebar{top:0}.theme-container.no-sidebar .sidebar{display:none}@media (max-width: 719px){.theme-container.no-sidebar .sidebar{display:block}}.theme-container.no-sidebar .page{padding-left:0}.theme-default-content a:hover{text-decoration:underline}.theme-default-content img{max-width:100%}.theme-default-content h1,.theme-default-content h2,.theme-default-content h3,.theme-default-content h4,.theme-default-content h5,.theme-default-content h6{margin-top:calc(.5rem - var(--navbar-height));padding-top:calc(1rem + var(--navbar-height));margin-bottom:0}.theme-default-content h1:first-child,.theme-default-content h2:first-child,.theme-default-content h3:first-child,.theme-default-content h4:first-child,.theme-default-content h5:first-child,.theme-default-content h6:first-child{margin-bottom:1rem}.theme-default-content h1:first-child+p,.theme-default-content h1:first-child+pre,.theme-default-content h1:first-child+.custom-container,.theme-default-content h2:first-child+p,.theme-default-content h2:first-child+pre,.theme-default-content h2:first-child+.custom-container,.theme-default-content h3:first-child+p,.theme-default-content h3:first-child+pre,.theme-default-content h3:first-child+.custom-container,.theme-default-content h4:first-child+p,.theme-default-content h4:first-child+pre,.theme-default-content h4:first-child+.custom-container,.theme-default-content h5:first-child+p,.theme-default-content h5:first-child+pre,.theme-default-content h5:first-child+.custom-container,.theme-default-content h6:first-child+p,.theme-default-content h6:first-child+pre,.theme-default-content h6:first-child+.custom-container{margin-top:2rem}@media (max-width: 959px){.sidebar{font-size:15px;width:var(--sidebar-width-mobile)}.page{padding-left:var(--sidebar-width-mobile)}}@media (max-width: 719px){.sidebar{top:0;padding-top:var(--navbar-height);transform:translate(-100%)}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translate(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width: 419px){h1{font-size:1.9rem}}.navbar{--navbar-line-height: calc( var(--navbar-height) - 2 * var(--navbar-padding-v) );padding:var(--navbar-padding-v) var(--navbar-padding-h);line-height:var(--navbar-line-height)}.navbar .logo{height:var(--navbar-line-height);margin-right:var(--navbar-padding-v);vertical-align:top}.navbar .site-name{font-size:1.3rem;font-weight:600;color:var(--c-text);position:relative}.navbar .navbar-items-wrapper{display:flex;position:absolute;box-sizing:border-box;top:var(--navbar-padding-v);right:var(--navbar-padding-h);height:var(--navbar-line-height);padding-left:var(--navbar-padding-h);white-space:nowrap;font-size:.9rem}.navbar .navbar-items-wrapper .search-box{flex:0 0 auto;vertical-align:top}@media screen and (max-width: 719px){.navbar{padding-left:4rem}.navbar .can-hide{display:none}.navbar .site-name{width:calc(100vw - 9.4rem);overflow:hidden;white-space:nowrap;text-overflow:ellipsis}}.navbar-items{display:inline-block}@media print{.navbar-items{display:none}}.navbar-items a{display:inline-block;line-height:1.4rem;color:inherit}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text)}.navbar-items .navbar-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:var(--navbar-line-height)}.navbar-items .navbar-item:first-child{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:-2px;border-bottom:2px solid var(--c-text-accent)}@media (max-width: 719px){.navbar-items .navbar-item{margin-left:0}.navbar-items .navbar-item>a:hover,.navbar-items .navbar-item>a.router-link-active{margin-bottom:0;border-bottom:none}.navbar-items a:hover,.navbar-items a.router-link-active{color:var(--c-text-accent)}}.toggle-sidebar-button{position:absolute;top:.6rem;left:1rem;display:none;padding:.6rem;cursor:pointer}.toggle-sidebar-button .icon{display:flex;flex-direction:column;justify-content:center;align-items:center;width:1.25rem;height:1.25rem;cursor:inherit}.toggle-sidebar-button .icon span{display:inline-block;width:100%;height:2px;border-radius:2px;background-color:var(--c-text);transition:transform var(--t-transform)}.toggle-sidebar-button .icon span:nth-child(2){margin:6px 0}@media screen and (max-width: 719px){.toggle-sidebar-button{display:block}}.toggle-color-mode-button{display:flex;margin:auto;margin-left:1rem;border:0;background:none;color:var(--c-text);opacity:.8;cursor:pointer}@media print{.toggle-color-mode-button{display:none}}.toggle-color-mode-button:hover{opacity:1}.toggle-color-mode-button .icon{width:1.25rem;height:1.25rem}.DocSearch{transition:background-color var(--t-color)}.navbar-dropdown-wrapper{cursor:pointer}.navbar-dropdown-wrapper .navbar-dropdown-title,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:var(--c-text)}.navbar-dropdown-wrapper .navbar-dropdown-title:hover,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{border-color:transparent}.navbar-dropdown-wrapper .navbar-dropdown-title .arrow,.navbar-dropdown-wrapper .navbar-dropdown-title-mobile .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile{display:none;font-weight:600;font-size:inherit}.navbar-dropdown-wrapper .navbar-dropdown-title-mobile:hover{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item{color:inherit;line-height:1.7rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{margin:.45rem 0 0;border-top:1px solid var(--c-border);padding:1rem 0 .45rem;font-size:.9rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>span{padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a{font-weight:inherit}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle>a.router-link-active:after{display:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper{padding:0;list-style:none}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem-wrapper .navbar-dropdown-subitem{font-size:.9em}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a:hover,.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active{color:var(--c-text-accent)}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{content:"";width:0;height:0;border-left:5px solid var(--c-text-accent);border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item:first-child .navbar-dropdown-subtitle{margin-top:0;padding-top:0;border-top:0}.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile.open .navbar-dropdown-title-mobile{margin-bottom:.5rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title,.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:none}.navbar-dropdown-wrapper.mobile .navbar-dropdown-title-mobile{display:block}.navbar-dropdown-wrapper.mobile .navbar-dropdown{transition:height .1s ease-out;overflow:hidden}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle{border-top:0;margin-top:0;padding-top:0;padding-bottom:0}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subtitle,.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item>a{font-size:15px;line-height:2rem}.navbar-dropdown-wrapper.mobile .navbar-dropdown .navbar-dropdown-item .navbar-dropdown-subitem{font-size:14px;padding-left:1rem}.navbar-dropdown-wrapper:not(.mobile){height:1.8rem}.navbar-dropdown-wrapper:not(.mobile):hover .navbar-dropdown,.navbar-dropdown-wrapper:not(.mobile).open .navbar-dropdown{display:block!important}.navbar-dropdown-wrapper:not(.mobile).open:blur{display:none}.navbar-dropdown-wrapper:not(.mobile) .navbar-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:var(--c-bg-navbar);padding:.6rem 0;border:1px solid var(--c-border);border-bottom-color:var(--c-border-dark);text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}.page{padding-bottom:2rem;display:block}.page .theme-default-content{max-width:var(--content-width);margin:0 auto;padding:2rem 2.5rem;padding-top:0}@media (max-width: 959px){.page .theme-default-content{padding:2rem}}@media (max-width: 419px){.page .theme-default-content{padding:1.5rem}}.page-meta{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem;overflow:auto}@media (max-width: 959px){.page-meta{padding:2rem}}@media (max-width: 419px){.page-meta{padding:1.5rem}}.page-meta .meta-item{cursor:default;margin-top:.8rem}.page-meta .meta-item .meta-item-label{font-weight:500;color:var(--c-text-lighter)}.page-meta .meta-item .meta-item-info{font-weight:400;color:var(--c-text-quote)}.page-meta .edit-link{display:inline-block;margin-right:.25rem}@media print{.page-meta .edit-link{display:none}}.page-meta .last-updated{float:right}@media (max-width: 719px){.page-meta .last-updated{font-size:.8em;float:none}.page-meta .contributors{font-size:.8em}}.page-nav{max-width:var(--content-width);margin:0 auto;padding:1rem 2.5rem 2rem;padding-bottom:0}@media (max-width: 959px){.page-nav{padding:2rem}}@media (max-width: 419px){.page-nav{padding:1.5rem}}.page-nav .inner{min-height:2rem;margin-top:0;border-top:1px solid var(--c-border);transition:border-color var(--t-color);padding-top:1rem;overflow:auto}.page-nav .prev a:before{content:"\2190"}.page-nav .next{float:right}.page-nav .next a:after{content:"\2192"}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .navbar-items{display:none;border-bottom:1px solid var(--c-border);transition:border-color var(--t-color);padding:.5rem 0 .75rem}.sidebar .navbar-items a{font-weight:600}.sidebar .navbar-items .navbar-item{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar .sidebar-items{padding:1.5rem 0}@media (max-width: 719px){.sidebar .navbar-items{display:block}.sidebar .navbar-items .navbar-dropdown-wrapper .navbar-dropdown .navbar-dropdown-item a.router-link-active:after{top:calc(1rem - 2px)}.sidebar .sidebar-items{padding:1rem 0}}.sidebar-item{cursor:default;border-left:.25rem solid transparent;color:var(--c-text)}.sidebar-item:focus-visible{outline-width:1px;outline-offset:-1px}.sidebar-item.active:not(p.sidebar-heading){font-weight:600;color:var(--c-text-accent);border-left-color:var(--c-text-accent)}.sidebar-item.sidebar-heading{transition:color .15s ease;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0}.sidebar-item.sidebar-heading+.sidebar-item-children{transition:height .1s ease-out;overflow:hidden;margin-bottom:.75rem}.sidebar-item.collapsible{cursor:pointer}.sidebar-item.collapsible .arrow{position:relative;top:-.12em;left:.5em}.sidebar-item:not(.sidebar-heading){font-size:1em;font-weight:400;display:inline-block;margin:0;padding:.35rem 1rem .35rem 2rem;line-height:1.4;width:100%;box-sizing:border-box}.sidebar-item:not(.sidebar-heading)+.sidebar-item-children{padding-left:1rem;font-size:.95em}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading){padding:.25rem 1rem .25rem 1.75rem}.sidebar-item-children .sidebar-item-children .sidebar-item:not(.sidebar-heading).active{font-weight:500;border-left-color:transparent}a.sidebar-heading+.sidebar-item-children .sidebar-item:not(.sidebar-heading).active{border-left-color:transparent}a.sidebar-item{cursor:pointer}a.sidebar-item:hover{color:var(--c-text-accent)}.table-of-contents .badge{vertical-align:middle}.dropdown-enter-from,.dropdown-leave-to{height:0!important}.fade-slide-y-enter-active{transition:all .2s ease}.fade-slide-y-leave-active{transition:all .2s cubic-bezier(1,.5,.8,1)}.fade-slide-y-enter-from,.fade-slide-y-leave-to{transform:translateY(10px);opacity:0}html.dark{--c-warning-bg: #6a4b16;--c-warning-title: #fff4ce;--c-warning-text: #e6e6e6;--c-danger-bg: #5a0001;--c-danger-title: #fde7e9;--c-danger-text: #e6e6e6}html.dark .custom-container a{color:var(--c-text-accent)}html.dark code{color:#e6e6e6}code{padding:.1em .2em}body .sidebar-item:not(.sidebar-heading){padding:.15rem 1rem .15rem 2rem}body .sidebar-item.sidebar-heading{padding:.5rem 1.5rem .1rem 1.25rem}body .sidebar .sidebar-items{padding-top:.5rem;padding-bottom:5rem}body .wide-page .page .theme-default-content{max-width:1700px!important}body td:has(> div[class*=language-]:only-child:not(.line-numbers-mode)){padding:0}body td:has(> div[class*=language-]:only-child:not(.line-numbers-mode))>div{background-color:transparent!important}body td:has(> div[class*=language-]:only-child:not(.line-numbers-mode))>div:before{top:0;right:.5em}body td:has(> div[class*=language-]:only-child:not(.line-numbers-mode)) .shiki{margin-top:0;margin-bottom:0}/*! @docsearch/css 3.3.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,.3);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,.5),0 -4px 8px 0 rgba(0,0,0,.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;position:relative;padding:0 0 2px;border:0;top:-1px;width:20px}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::-moz-placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:focus{outline:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"\bb "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border-radius:2px;box-shadow:var(--docsearch-key-shadow);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;color:var(--docsearch-muted-color);border:0;width:20px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@media (min-width: 751px){#docsearch-container{min-width:171.36px}}@media (max-width: 750px){.DocSearch-Container{position:fixed}#docsearch-container{min-width:52px}}@media print{#docsearch-container{display:none}}.code-tabs__nav{margin-top:.85rem;margin-bottom:calc(-1.7rem - 6px);padding-bottom:calc(1.7rem - 6px);padding-left:10px;padding-top:10px;border-top-left-radius:6px;border-top-right-radius:6px;background-color:var(--code-bg-color)}.code-tabs__ul{margin:auto 0 5px;padding-left:0;display:inline-flex;list-style:none}.code-tabs__nav-tab{border:0;padding:5px 10px;cursor:pointer;background-color:transparent;font-size:.9em;line-height:1.4;color:#ffffffe6;font-weight:600}.code-tabs__nav-tab:focus{outline:none}.code-tabs__nav-tab:focus-visible{outline:1px solid rgba(255,255,255,.9)}.code-tabs__nav-tab-active{border-bottom:var(--c-brand) 2px solid}@media (max-width: 500px){.code-tabs__nav{margin-left:-1.5rem;margin-right:-1.5rem;border-radius:0}}.code-tabs-item{color:#adbac7;background-color:var(--code-bg-color)}.code-tabs-item div[class*=language-]:before{top:5px}.code-tabs-item>:not(.line-numbers-mode){padding:0 20px}.code-tabs-item pre,.code-tabs-item pre+div{padding-top:8px!important}.code-prop{margin-top:10px}.code-prop .shiki{margin:0;padding:2px 8px;white-space:normal}.code-prop .shiki .line{display:block;white-space:pre-wrap}.code-prop+:not(.code-prop,h1,h2,h3,h4,h5),.code-prop+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5),.code-prop+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5),.code-prop+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5),.code-prop+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5)+:not(.code-prop,h1,h2,h3,h4,h5){margin-top:4px;margin-left:20px} diff --git a/assets/typescript-partial.html.544452e0.js b/assets/typescript-partial.html.544452e0.js new file mode 100644 index 000000000..b553ba2e5 --- /dev/null +++ b/assets/typescript-partial.html.544452e0.js @@ -0,0 +1,8 @@ +import{_ as s,y as a,z as n,Q as t,a5 as l,X as o,P as i}from"./framework.fe9a73df.js";const r={},p=l(`

    [TypeScriptPartial]

    Note

    This attribute only applies to the Knockout front-end stack. It is not applicable to the Vue stack.

    If defined on a model, a typescript file will be generated in ./Scripts/Partials if one does not already exist. This 'Partial' TypeScript file contains a class which inherits from the generated TypeScript ViewModel. The partial class has the same name as the generated ViewModel would normally have, and the generated ViewModel is renamed to "<ClassName>Partial".

    This behavior allows you to extend the behavior of the generated TypeScript view models with your own properties and methods for defining more advanced behavior on the client. One of the most common use cases of this is to define additional Knockout ComputedObservable properties for information that is only useful in the browser - for example, computing a css class based on data in the object.

    Example Usage

    [TypeScriptPartial]
    +public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +
    +    ...
    +}
    +

    Properties

    `,7),c=o("p",null,"If set, overrides the name of the generated ViewModel which becomes the base class for the generated 'Partial' TypeScript file.",-1);function d(h,D){const e=i("Prop");return a(),n("div",null,[p,t(e,{def:"public string BaseClassName { get; set; }"}),c])}const y=s(r,[["render",d],["__file","typescript-partial.html.vue"]]);export{y as default}; diff --git a/assets/typescript-partial.html.d72d268f.js b/assets/typescript-partial.html.d72d268f.js new file mode 100644 index 000000000..3da42e38f --- /dev/null +++ b/assets/typescript-partial.html.d72d268f.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-1a8eae00","path":"/modeling/model-components/attributes/typescript-partial.html","title":"[TypeScriptPartial]","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Example Usage","slug":"example-usage","link":"#example-usage","children":[]},{"level":2,"title":"Properties","slug":"properties","link":"#properties","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"modeling/model-components/attributes/typescript-partial.md"}');export{e as data}; diff --git a/assets/view-model.html.51107651.js b/assets/view-model.html.51107651.js new file mode 100644 index 000000000..24d24a133 --- /dev/null +++ b/assets/view-model.html.51107651.js @@ -0,0 +1 @@ +import{_ as i,y as n,z as c,X as e,B as t,Q as a,$ as s,P as r}from"./framework.fe9a73df.js";const d={},l=e("h1",{id:"typescript-viewmodels",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typescript-viewmodels","aria-hidden":"true"},"#"),t(" TypeScript ViewModels")],-1),h=e("p",null,"This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.",-1),u=e("h2",{id:"vue",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#vue","aria-hidden":"true"},"#"),t(" Vue")],-1),_=e("h2",{id:"knockout",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#knockout","aria-hidden":"true"},"#"),t(" Knockout")],-1);function p(m,f){const o=r("RouterLink");return n(),c("div",null,[l,h,u,e("p",null,[t("See: "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[t("Vue ViewModels")]),_:1})]),_,e("p",null,[t("See: "),a(o,{to:"/stacks/ko/client/view-model.html"},{default:s(()=>[t("Knockout ViewModels")]),_:1})])])}const v=i(d,[["render",p],["__file","view-model.html.vue"]]);export{v as default}; diff --git a/assets/view-model.html.9f81154a.js b/assets/view-model.html.9f81154a.js new file mode 100644 index 000000000..50e3b7622 --- /dev/null +++ b/assets/view-model.html.9f81154a.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0f1441d8","path":"/stacks/ko/client/view-model.html","title":"TypeScript ViewModels","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Base Members","slug":"base-members","link":"#base-members","children":[]},{"level":2,"title":"Model-Specific Members","slug":"model-specific-members","link":"#model-specific-members","children":[{"level":3,"title":"Configuration","slug":"configuration","link":"#configuration","children":[]},{"level":3,"title":"DataSources","slug":"datasources","link":"#datasources","children":[]},{"level":3,"title":"Data Properties","slug":"data-properties","link":"#data-properties","children":[]},{"level":3,"title":"Enum Members","slug":"enum-members","link":"#enum-members","children":[]},{"level":3,"title":"Collection Navigation Property Helpers","slug":"collection-navigation-property-helpers","link":"#collection-navigation-property-helpers","children":[]},{"level":3,"title":"Reference Navigation Property Helpers","slug":"reference-navigation-property-helpers","link":"#reference-navigation-property-helpers","children":[]},{"level":3,"title":"Instance Method Members","slug":"instance-method-members","link":"#instance-method-members","children":[]}]}],"git":{"updatedTime":1663268139000},"filePathRelative":"stacks/ko/client/view-model.md"}');export{e as data}; diff --git a/assets/view-model.html.b9d16d3b.js b/assets/view-model.html.b9d16d3b.js new file mode 100644 index 000000000..5e22d66f7 --- /dev/null +++ b/assets/view-model.html.b9d16d3b.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-0c9f605a","path":"/stacks/disambiguation/view-model.html","title":"TypeScript ViewModels","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"Vue","slug":"vue","link":"#vue","children":[]},{"level":2,"title":"Knockout","slug":"knockout","link":"#knockout","children":[]}],"git":{"updatedTime":1652390840000},"filePathRelative":"stacks/disambiguation/view-model.md"}');export{e as data}; diff --git a/assets/view-model.html.f673f519.js b/assets/view-model.html.f673f519.js new file mode 100644 index 000000000..0e413ed18 --- /dev/null +++ b/assets/view-model.html.f673f519.js @@ -0,0 +1,21 @@ +import{_ as d,y as c,z as u,X as e,B as t,Q as o,$ as a,P as i}from"./framework.fe9a73df.js";const h={},p=e("h1",{id:"typescript-viewmodels",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#typescript-viewmodels","aria-hidden":"true"},"#"),t(" TypeScript ViewModels")],-1),f=e("p",null,"For each database-mapped type in your model, Coalesce will generate a TypeScript class that provides a multitude of functionality for interacting with the data on the client.",-1),b={href:"https://knockoutjs.com/",target:"_blank",rel:"noopener noreferrer"},m={class:"table-of-contents"},g=e("h2",{id:"base-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#base-members","aria-hidden":"true"},"#"),t(" Base Members")],-1),_=e("p",null,"The following base members are available to all generated ViewModel classes:",-1),v=e("p",null,"Flag to use to determine if this item is checked. Only provided for convenience.",-1),y=e("p",null,"Flag to use to determine if this item is selected. Only provided for convenience.",-1),w=e("p",null,"Flag to use to determine if this item is being edited. Only provided for convenience.",-1),k=e("p",null,[t("Toggles the "),e("code",null,"isEditing"),t(" flag.")],-1),C=e("p",null,"Flag to use to determine if this item is expanded. Only provided for convenience.",-1),M=e("p",null,[t("Toggles the "),e("code",null,"isExpanded"),t(" flag.")],-1),S=e("p",null,"Flag to use to determine if this item is shown. Only provided for convenience.",-1),O=e("p",null,[t("Toggles the "),e("code",null,"isSelected"),t(" flag.")],-1),T=e("p",null,"Sets isSelected(true) on this object and clears on the rest of the items in the parent collection.",-1),x=e("p",null,"Dirty Flag. Set when a value on the model changes. Reset when the model is saved or reloaded.",-1),K=e("p",null,"True once the data has been loaded.",-1),j=e("p",null,"True if the object is loading.",-1),P=e("p",null,"True if the object is currently saving.",-1),V=e("p",null,"Returns true if the current object, or any of its children, are saving.",-1),D=e("p",null,"Loads the object from the server based on the id specified. If no id is specified, the current id, is used if one is set.",-1),F=e("p",null,"Loads any child objects that have an ID set, but not the full object. This is useful when creating an object that has a parent object and the ID is set on the new child.",-1),I=e("p",null,"Loads this object from a data transfer object received from the server.",-1),E=e("ul",null,[e("li",null,[e("code",null,"force"),t(" - Will override the check against isLoading that is done to prevent recursion.")]),e("li",null,[e("code",null,"allowCollectionDeletes"),t(" - Set true when entire collections are loaded. True is the default. In some cases only a partial collection is returned, set to false to only add/update collections.")])],-1),L=e("p",null,"Deletes the object without any prompt for confirmation.",-1),A=e("p",null,"Deletes the object if a prompt for confirmation is answered affirmatively.",-1),B=e("p",null,"Contains the error message from the last failed call to the server.",-1),R=e("p",null,[t("Register a callback to be called when a save is done. Returns "),e("code",null,"true"),t(" if the callback was registered, or "),e("code",null,"false"),t(" if the callback was already registered.")],-1),N=e("p",null,"Saves this object into a data transfer object to send to the server.",-1),Q=e("p",null,"Saves the object to the server and then calls a callback. Returns false if there are validation errors.",-1),G=e("p",null,"Parent of this object, if this object was loaded as part of a hierarchy.",-1),H=e("p",null,"Parent of this object, if this object was loaded as part of list of objects.",-1),J=e("p",null,"URL to a stock editor for this object.",-1),U=e("p",null,"Displays an editor for the object in a modal dialog.",-1),W=e("p",null,"Triggers any validation messages to be shown, and returns a bool that indicates if there are any validation errors.",-1),q=e("p",null,"ValidationIssues returned from the server when trying to persist data",-1),z=e("p",null,"List of warnings found during validation. Saving is still allowed with warnings present.",-1),X=e("p",null,"List of errors found during validation. Any errors present will prevent saving.",-1),$=e("h2",{id:"model-specific-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#model-specific-members","aria-hidden":"true"},"#"),t(" Model-Specific Members")],-1),Y=e("p",null,[t("The following members are generated for each generated ViewModel class and are unique to each class. The examples below are based on a type named "),e("code",null,"Person"),t(".")],-1),Z=e("h3",{id:"configuration",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#configuration","aria-hidden":"true"},"#"),t(" Configuration")],-1),ee=e("h3",{id:"datasources",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#datasources","aria-hidden":"true"},"#"),t(" DataSources")],-1),te=e("code",null,"ListViewModels.DataSources",-1),oe=e("code",null,"ViewModel",-1),ne=e("code",null,"ListViewModel",-1),ae=e("code",null,"dataSources",-1),le=e("code",null,"dataSource",-1),se=e("h3",{id:"data-properties",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#data-properties","aria-hidden":"true"},"#"),t(" Data Properties")],-1),ie=e("p",null,[t("For each exposed property on the underlying EF POCO, a "),e("code",null,"KnockoutObservable"),t(" property will exist on the TypeScript model. For navigation properties, these will be typed with the corresponding TypeScript ViewModel for the other end of the relationship. For collections (including collection navigation properties), these properties will be "),e("code",null,"KnockoutObservableArray"),t(" objects.")],-1),re=e("h3",{id:"enum-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#enum-members","aria-hidden":"true"},"#"),t(" Enum Members")],-1),de=e("p",null,[t("For each "),e("code",null,"enum"),t(" property on your POCO, the following will be created:")],-1),ce=e("p",null,[t("A "),e("code",null,"KnockoutComputed"),t(" property that will provide the text to display for that property.")],-1),ue=e("p",null,[t("A static array of objects with properties "),e("code",null,"id"),t(" and "),e("code",null,"value"),t(" that represent all the values of the enum.")],-1),he=e("p",null,[t("A TypeScript enum that mirrors the C# enum directly. This enum is in a sub-namespace of "),e("code",null,"ViewModels"),t(" named the same as the class name.")],-1),pe=e("h3",{id:"collection-navigation-property-helpers",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#collection-navigation-property-helpers","aria-hidden":"true"},"#"),t(" Collection Navigation Property Helpers")],-1),fe=e("p",null,"For each collection navigation property on the POCO, the following members will be created:",-1),be=e("code",null,"autoSave",-1),me=e("p",null,[t("A "),e("code",null,"KnockoutComputed"),t(" that evaluates to a relative url for the generated table view that contains only the items that belong to the collection navigation property.")],-1),ge=e("h3",{id:"reference-navigation-property-helpers",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#reference-navigation-property-helpers","aria-hidden":"true"},"#"),t(" Reference Navigation Property Helpers")],-1),_e=e("p",null,[t("For each reference navigation property on the POCO a method will be created that will call "),e("code",null,"showEditor"),t(" on that current value of the navigation property, or on a new instance if the current value is null.")],-1),ve=e("code",null,"KnockoutComputed",-1),ye=e("h3",{id:"instance-method-members",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#instance-method-members","aria-hidden":"true"},"#"),t(" Instance Method Members")],-1);function we(ke,Ce){const r=i("ExternalLinkIcon"),l=i("router-link"),n=i("Prop"),s=i("RouterLink");return c(),u("div",null,[p,f,e("p",null,[t("These ViewModels are dependent on "),e("a",b,[t("Knockout"),o(r)]),t(", and are designed to be used directly from Knockout bindings in your HTML. All data properties on the generated model are Knockout observables.")]),e("nav",m,[e("ul",null,[e("li",null,[o(l,{to:"#base-members"},{default:a(()=>[t("Base Members")]),_:1})]),e("li",null,[o(l,{to:"#model-specific-members"},{default:a(()=>[t("Model-Specific Members")]),_:1}),e("ul",null,[e("li",null,[o(l,{to:"#configuration"},{default:a(()=>[t("Configuration")]),_:1})]),e("li",null,[o(l,{to:"#datasources"},{default:a(()=>[t("DataSources")]),_:1})]),e("li",null,[o(l,{to:"#data-properties"},{default:a(()=>[t("Data Properties")]),_:1})]),e("li",null,[o(l,{to:"#enum-members"},{default:a(()=>[t("Enum Members")]),_:1})]),e("li",null,[o(l,{to:"#collection-navigation-property-helpers"},{default:a(()=>[t("Collection Navigation Property Helpers")]),_:1})]),e("li",null,[o(l,{to:"#reference-navigation-property-helpers"},{default:a(()=>[t("Reference Navigation Property Helpers")]),_:1})]),e("li",null,[o(l,{to:"#instance-method-members"},{default:a(()=>[t("Instance Method Members")]),_:1})])])])])]),g,_,o(n,{def:"includes: string",lang:"ts"}),e("p",null,[t("String that will be passed to the server when loading and saving that allows for data trimming via C# Attributes. See "),o(s,{to:"/concepts/includes.html"},{default:a(()=>[t("Includes String")]),_:1}),t(".")]),o(n,{def:"isChecked: KnockoutObservable",lang:"ts"}),v,o(n,{def:"isSelected: KnockoutObservable",lang:"ts"}),y,o(n,{def:"isEditing: KnockoutObservable",lang:"ts"}),w,o(n,{def:"toggleIsEditing () => void",lang:"ts"}),k,o(n,{def:"isExpanded: KnockoutObservable",lang:"ts"}),C,o(n,{def:"toggleIsExpanded: () => void",lang:"ts"}),M,o(n,{def:"isVisible: KnockoutObservable",lang:"ts"}),S,o(n,{def:"toggleIsSelected () => void",lang:"ts"}),O,o(n,{def:"selectSingle: (): boolean",lang:"ts"}),T,o(n,{def:"isDirty: KnockoutObservable",lang:"ts"}),x,o(n,{def:"isLoaded: KnockoutObservable",lang:"ts"}),K,o(n,{def:"isLoading: KnockoutObservable",lang:"ts"}),j,o(n,{def:"isSaving: KnockoutObservable",lang:"ts"}),P,o(n,{def:"isThisOrChildSaving: KnockoutComputed",lang:"ts"}),V,o(n,{def:"load: id: any, callback?: (self: T) => void): JQueryPromise | undefined",lang:"ts"}),D,o(n,{def:"loadChildren: callback?: () => void) => void",lang:"ts"}),F,o(n,{def:"loadFromDto: data: any, force?: boolean, allowCollectionDeletes?: boolean) => void",lang:"ts"}),I,E,o(n,{def:"deleteItem: callback?: (self: T) => void): JQueryPromise | undefined",lang:"ts"}),L,o(n,{def:"deleteItemWithConfirmation: callback?: () => void, message?: string): JQueryPromise | undefined",lang:"ts"}),A,o(n,{def:"errorMessage: KnockoutObservable",lang:"ts"}),B,o(n,{def:"onSave: callback: (self: T) => void): boolean",lang:"ts"}),R,o(n,{def:"saveToDto: () => any",lang:"ts"}),N,o(n,{def:"save: callback?: (self: T) => void): JQueryPromise | boolean | undefined",lang:"ts"}),Q,o(n,{def:"parent: any",lang:"ts"}),G,o(n,{def:"parentCollection: KnockoutObservableArray",lang:"ts"}),H,o(n,{def:"editUrl: KnockoutComputed",lang:"ts"}),J,o(n,{def:"showEditor: callback?: any): JQueryPromise",lang:"ts"}),U,o(n,{def:"validate: (): boolean",lang:"ts"}),W,o(n,{def:"validationIssues: any",lang:"ts"}),q,o(n,{def:"warnings: KnockoutValidationErrors",lang:"ts"}),z,o(n,{def:"errors: KnockoutValidationErrors",lang:"ts"}),X,$,Y,Z,o(n,{def:"static coalesceConfig: Coalesce.ViewModelConfiguration",lang:"ts",id:"member-class-config"}),e("p",null,[t("A static configuration object for configuring all instances of the ViewModel's type is created. See "),o(s,{to:"/stacks/ko/client/model-config.html"},{default:a(()=>[t("ViewModel Configuration")]),_:1}),t(".")]),o(n,{def:"coalesceConfig: Coalesce.ViewModelConfiguration",lang:"ts",id:"member-instance-config"}),e("p",null,[t("An per-instance configuration object for configuring each specific ViewModel instance is created. See "),o(s,{to:"/stacks/ko/client/model-config.html"},{default:a(()=>[t("ViewModel Configuration")]),_:1}),t(".")]),ee,o(n,{def:` +public dataSources = ListViewModels.PersonDataSources; +public dataSource: DataSource = new this.dataSources.Default();`,lang:"ts",id:"code-data-source-members"}),e("p",null,[t("For each of the "),o(s,{to:"/modeling/model-components/data-sources.html"},{default:a(()=>[t("Data Sources")]),_:1}),t(" for a model, a class will be added to a namespace named "),te,t(". This namespace can always be accessed on both "),oe,t(" and "),ne,t(" instances via the "),ae,t(" property, and class instances can be assigned to the "),le,t(" property.")]),se,o(n,{def:` +public personId: KnockoutObservable = ko.observable(null); +public fullName: KnockoutObservable = ko.observable(null); +public gender: KnockoutObservable = ko.observable(null); +public companyId: KnockoutObservable = ko.observable(null); +public company: KnockoutObservable = ko.observable(null); +public addresses: KnockoutObservableArray = ko.observableArray([]); +public birthDate: KnockoutObservable = ko.observable(moment());`,lang:"ts",id:"code-data-members"}),ie,re,de,o(n,{def:"public genderText: KnockoutComputed",lang:"ts"}),ce,o(n,{def:`public genderValues: Coalesce.EnumValue[] = [ + { id: 1, value: 'Male' }, + { id: 2, value: 'Female' }, + { id: 3, value: 'Other' }, +];`,lang:"ts",id:"code-enum-members"}),ue,o(n,{def:`export namespace Person { + export enum GenderEnum { + Male = 1, + Female = 2, + Other = 3, + }; +}`,lang:"ts","no-class":"",id:"code-enum-def"}),he,pe,fe,o(n,{def:"public addToAddresses: (autoSave?: boolean) => ViewModels.Address;",lang:"ts"}),e("p",null,[t("A method that will add a new object to that collection property. If "),be,t(" is specified, the auto-save behavior of the new object will be set to that value. Otherwise, the inherited default will be used (see "),o(s,{to:"/stacks/ko/client/model-config.html"},{default:a(()=>[t("ViewModel Configuration")]),_:1}),t(")")]),o(n,{def:"public addressesListUrl: KnockoutComputed;",lang:"ts"}),me,ge,o(n,{def:"public showCompanyEditor: (callback?: any) => void;",lang:"ts"}),_e,o(n,{def:"public companyText: KnockoutComputed;",lang:"ts"}),e("p",null,[t("For each reference navigation property, a "),ve,t(" property will be created that will provide the text to display for that property. This will be the property on the class annotated with "),o(s,{to:"/modeling/model-components/attributes/list-text.html"},{default:a(()=>[t("[ListText]")]),_:1}),t(".")]),ye,o(n,{def:`public readonly getBirthDate = new Person.GetBirthDate(this); +public static GetBirthDate = class GetBirthDate extends Coalesce.ClientMethod { ... };`,lang:"ts",id:"code-instance-method-members"}),e("p",null,[t("For each "),o(s,{to:"/modeling/model-components/methods.html"},{default:a(()=>[t("Instance Method")]),_:1}),t(" on your POCO, a class and instance member will be created as described in "),o(s,{to:"/stacks/ko/client/methods.html"},{default:a(()=>[t("Methods - Generated TypeScript")]),_:1}),t(".")])])}const Se=d(h,[["render",we],["__file","view-model.html.vue"]]);export{Se as default}; diff --git a/assets/viewmodels.html.c2654093.js b/assets/viewmodels.html.c2654093.js new file mode 100644 index 000000000..a74be168d --- /dev/null +++ b/assets/viewmodels.html.c2654093.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-25ac4e91","path":"/stacks/vue/layers/viewmodels.html","title":"Vue ViewModel Layer","lang":"en-US","frontmatter":{},"excerpt":"","headers":[{"level":2,"title":"ViewModels","slug":"viewmodels","link":"#viewmodels","children":[{"level":3,"title":"Model Data Properties","slug":"model-data-properties","link":"#model-data-properties","children":[]},{"level":3,"title":"Other Data Properties & Functions","slug":"other-data-properties-functions","link":"#other-data-properties-functions","children":[]},{"level":3,"title":"Loading & Parameters","slug":"loading-parameters","link":"#loading-parameters","children":[]},{"level":3,"title":"Saving and Deleting","slug":"saving-and-deleting","link":"#saving-and-deleting","children":[]},{"level":3,"title":"Auto-save","slug":"auto-save","link":"#auto-save","children":[]},{"level":3,"title":"Bulk saves","slug":"bulk-saves","link":"#bulk-saves","children":[]},{"level":3,"title":"Rules/Validation","slug":"rules-validation","link":"#rules-validation","children":[]},{"level":3,"title":"Generated Members","slug":"generated-members","link":"#generated-members","children":[]}]},{"level":2,"title":"ListViewModels","slug":"listviewmodels","link":"#listviewmodels","children":[{"level":3,"title":"Data Properties","slug":"data-properties","link":"#data-properties","children":[]},{"level":3,"title":"Parameters & API Callers","slug":"parameters-api-callers","link":"#parameters-api-callers","children":[]},{"level":3,"title":"Auto-Load","slug":"auto-load","link":"#auto-load","children":[]},{"level":3,"title":"Generated Members","slug":"generated-members-1","link":"#generated-members-1","children":[]}]},{"level":2,"title":"Service ViewModels","slug":"service-viewmodels","link":"#service-viewmodels","children":[{"level":3,"title":"Generated Members","slug":"generated-members-2","link":"#generated-members-2","children":[]}]}],"git":{"updatedTime":1694448005000},"filePathRelative":"stacks/vue/layers/viewmodels.md"}');export{e as data}; diff --git a/assets/viewmodels.html.db1146d4.js b/assets/viewmodels.html.db1146d4.js new file mode 100644 index 000000000..ef5b32377 --- /dev/null +++ b/assets/viewmodels.html.db1146d4.js @@ -0,0 +1,45 @@ +import{_ as p,y as u,z as h,W as c,X as t,B as e,Q as a,$ as s,a5 as i,P as r}from"./framework.fe9a73df.js";const m={},f=t("h1",{id:"vue-viewmodel-layer",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#vue-viewmodel-layer","aria-hidden":"true"},"#"),e(" Vue ViewModel Layer")],-1),y=t("code",null,"viewmodels.g.ts",-1),v=t("p",null,"These classes provide a wide array of functionality that is useful when interacting with your data model through a user interface. The generated ViewModels are the primary way that Coalesce is used when developing a Vue application.",-1),g={class:"table-of-contents"},_=t("h2",{id:"viewmodels",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#viewmodels","aria-hidden":"true"},"#"),e(" ViewModels")],-1),b=t("code",null,"viewmodels.g.ts",-1),w=t("code",null,"ViewModel",-1),D=t("h3",{id:"model-data-properties",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#model-data-properties","aria-hidden":"true"},"#"),e(" Model Data Properties")],-1),$=i('

    There are a few special behaviors when assigning to different kinds of data properties on View Models as well:

    Model Object Properties

    • If the object being assigned to the property is not a ViewModel instance, a new instance will be created automatically and used instead of the incoming object.
    • If the model property is a reference navigation, the corresponding foreign key property will automatically be set to the primary key of that object. If the incoming value was null, the foreign key will be set to null.
    • If deep auto-saves are enabled on the instance being assigned to, auto-save will be spread to the incoming object, and to all other objects reachable from that object.

    Model Collection Properties

    • When assigning an entire array, any items in the array that are not a ViewModel instance will have an instance created for them.
    • The same rule goes for pushing items into the existing array for a model collection - a new ViewModel instance will be created and be used instead of the object(s) being pushed.

    Foreign Key Properties

    If the corresponding navigation property contains an object, and that object's primary key doesn't match the new foreign key value being assigned, the navigation property will be set to null.

    Other Data Properties & Functions

    ',8),P=t("p",null,"An immutable number that is unique among all ViewModel instances, regardless of type.",-1),k=t("p",null,[e("Useful for uniquely identifying instances with "),t("code",null,':key="vm.$stableId"'),e(" in a Vue component, especially for instances that lack a primary key.")],-1),A=t("p",null,"A getter/setter property that wraps the primary key of the model. Used to interact with the primary key of any ViewModel in a polymorphic way.",-1),M=t("p",null,"Returns a string representation of the object, or one of its properties if specified, suitable for display.",-1),C=t("p",null,[e("Creates a new instance of an item for the specified child model collection, adds it to that collection, and returns the item. If "),t("code",null,"initialDirtyData"),e(" is provided, it will be loaded into the new instance with "),t("code",null,"$loadDirtyData()"),e(".")],-1),S=t("h3",{id:"loading-parameters",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#loading-parameters","aria-hidden":"true"},"#"),e(" Loading & Parameters")],-1),x=t("code",null,"/get",-1),T=t("code",null,"id",-1),V=t("code",null,"$primaryKey",-1),I=t("code",null,"$params",-1),E=t("code",null,"$load",-1),R=t("code",null,"$save",-1),j=t("code",null,"$bulkSave",-1),L=t("code",null,"$delete",-1),O=t("code",null,"$params.dataSource",-1),B=t("code",null,"$params.includes",-1),F=t("p",null,"Loads data from the provided model into the current ViewModel, and then clears all dirty flags.",-1),G=t("p",null,"Data is loaded recursively into all related ViewModel instances, preserving existing instances whose primary keys match the incoming data.",-1),N=t("p",null,[e("If auto-save is enabled, only non-dirty properties are updated. This prevents user input that is pending a save from being overwritten by the response from an auto-save "),t("code",null,"/save"),e(" request.")],-1),U=t("p",null,[e("If "),t("code",null,"purgeUnsaved"),e(" is true, items without a primary key will be dropped from collection navigation properties. This is used by the "),t("code",null,"$load"),e(" caller in order to fully reset the object graph with the state from the server.")],-1),W=t("p",null,[e("Same as "),t("code",null,"$loadCleanData"),e(", but does not clear any existing dirty flags, nor does it clear any dirty flags that will be set while mutating the data properties of any ViewModel instance that gets loaded.")],-1),z=t("p",null,[e("Create a new instance of the ViewModel, loading the given initial data with "),t("code",null,"$loadDirtyData()"),e(" if provided.")],-1),q=t("h3",{id:"saving-and-deleting",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#saving-and-deleting","aria-hidden":"true"},"#"),e(" Saving and Deleting")],-1),K=t("code",null,"/save",-1),X=t("code",null,"$params",-1),Y=t("a",{href:"#member-_bulksave"},"$bulkSave",-1),J=i('

    overrideProps can provide properties to save that override the data properties on the ViewModel instance. This allows for manually saving a change to a property without setting the property on the ViewModel instance into a dirty state. This makes it easier to handle some scenarios where changing the value of the property may put the UI into a logically inconsistent state until the save response has been returned from the server - for example, if a change to one property affects the computed value of other properties.

    When a save creates a new record and a new primary key is returned from the server, any entities attached to the current ViewModel via a collection navigation property will have their foreign keys set to the new primary key. This behavior, combined with the usage of deep auto-saves, allows for complex object graphs to be constructed even before any model in the graph has been created.

    When a save is in progress, the names of properties being saved are in contained in $savingProps.

    Saving behavior can be further customized with $loadResponseFromSaves and $saveMode, listed below.

    ',4),Q=t("code",null,"/delete",-1),H=t("code",null,"$params",-1),Z=t("p",null,"If the object was loaded as a child of a collection, it will be removed from that collection upon being deleted. Note that ViewModels currently only support tracking of a single parent collection, so if an object is programmatically added to additional collections, it will only be removed from one of them upon delete.",-1),ee=t("p",null,[e("Default "),t("code",null,"true"),e(" - controls if a ViewModel will be loaded with the data from the model returned by the "),t("code",null,"/save"),e(" endpoint when saved with the "),t("code",null,"$save"),e(" API caller. There is seldom any reason to disable this.")],-1),te=t("p",null,[e("When "),t("code",null,"$save.isLoading == true"),e(", contains the properties of the model currently being saved by "),t("code",null,"$save"),e(" (including auto-saves). Does not include non-dirty properties even if "),t("code",null,"$saveMode == 'whole'"),e(".")],-1),ae=t("p",null,"This can be used to make per-property UI state changes during saves - for example, displaying progress indicators on/near individual inputs, or disabling input controls.",-1),se=t("p",null,"Configures which properties of the model are sent to the server during a save or bulk save.",-1),oe={style:{"margin-left":"20px"}},ne=t("p",null,[t("code",null,'"surgical"'),e(" (default)")],-1),le=t("p",null,"By default, only dirty properties (and always the primary key) are sent to the server when performing a save.",-1),ie=t("p",null,"This improves the handling of concurrent changes being made by multiple users against different fields of the same entity at the same time - specifically, it prevents a user with a stale value of some field X from overwriting a more recent value of X in the database when the user is only making changes to some other property Y and has no intention of changing X.",-1),re=t("code",null,'"surgical"',-1),de={href:"https://docs.microsoft.com/en-us/ef/core/saving/concurrency",target:"_blank",rel:"noopener noreferrer"},ce={class:"custom-container warning"},pe=t("p",{class:"custom-container-title"},"WARNING",-1),ue=t("p",null,[e("Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the "),t("code",null,"x-www-form-urlencoded"),e(" body that is sent to the server.")],-1),he=t("p",null,[t("code",null,'"whole"')],-1),me=t("p",null,"All serializable properties of the object are sent back to the server with every save.",-1),fe=t("p",null,"Returns true if the given property is flagged as dirty.",-1),ye=t("p",null,"Manually set the dirty flag of the given property to the desired state. This seldom needs to be done explicitly, as mutating a property will automatically flag it as dirty.",-1),ve=t("p",null,[e("If "),t("code",null,"dirty"),e(" is true and "),t("code",null,"triggerAutoSave"),e(" is false, auto-save (if enabled) will not be immediately triggered for this specific flag change. Note that a future change to any other property's dirty flag will still trigger a save of all dirty properties.")],-1),ge=t("p",null,"Getter/setter that summarizes the model's property-level dirty flags. Returns true if any properties are dirty.",-1),_e=t("p",null,"When set to false, all property dirty flags are cleared. When set to true, all properties are marked as dirty.",-1),be=t("h3",{id:"auto-save",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#auto-save","aria-hidden":"true"},"#"),e(" Auto-save")],-1),we=t("code",null,"$save",-1),De=i(`
    type AutoSaveOptions<TThis> = 
    +{ 
    +    /** Time, in milliseconds, to debounce saves for.  */
    +    wait?: number;
    +    
    +    /** If true, auto-saving will also be enabled for all view models that are
    +        reachable from the navigation properties & collections of the current view model. */
    +    deep?: boolean;
    +
    +    /** Additional options to pass to the third parameter of lodash's \`debounce\` function. */
    +    debounce?: DebounceSettings;
    +
    +    /** A function that will be called before autosaving that can return false to prevent a save. 
    +        Only allowed if not using deep auto-saves.
    +    */
    +    predicate?: (viewModel: TThis) => boolean;
    +}
    +
    `,1),$e=t("p",null,[e("Turns off auto-saving of the instance. Does not recursively disable auto-saves on related instances if "),t("code",null,"deep"),e(" was used when auto-save was enabled.")],-1),Pe=t("p",null,"Returns true if auto-save is currently active on the instance.",-1),ke=t("h3",{id:"bulk-saves",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#bulk-saves","aria-hidden":"true"},"#"),e(" Bulk saves")],-1),Ae=t("p",null,"Bulk saves save all changes to an object graph in one API call and one database transaction. This includes creation, updates, and deletions of entities.",-1),Me=t("p",null,[e("To use bulk saves, you can work with your ViewModel instances on the client much in the same way you would on the server with Entity Framework. Assign objects to reference navigation properties and modify scalar values to perform creates and updates. To perform deletions, you must call "),t("code",null,"model.$remove()"),e(" on the ViewModel you want to remove, similar how you would call "),t("code",null,"DbSet<>.Remove(model)"),e(" on the server.")],-1),Ce=t("ul",null,[t("li",null,"All operations are wrapped in a single database transaction that is rolled back if any individual operation fails."),t("li",null,"Foreign keys will be fixed up as new items are created, allowing a parent and child record to be created at the same time even when the client has no foreign key to link the two together.")],-1),Se=t("code",null,"$bulkSave",-1),xe=t("code",null,"$params",-1),Te=t("p",null,"Removes the item from its parent collection (if it is in a collection), and marks the item for deletion in the next bulk save.",-1),Ve=t("p",null,[e("Returns true if the instance was previously removed by calling "),t("code",null,"$remove()"),e(".")],-1),Ie=t("h3",{id:"rules-validation",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#rules-validation","aria-hidden":"true"},"#"),e(" Rules/Validation")],-1),Ee=t("p",null,[e("Add a custom validation rule to the ViewModel for the specified property. "),t("code",null,"identifier"),e(" should be a short, unique slug that describes the rule; it is not displayed in the UI, but is used if you wish to later remove the rule with "),t("code",null,"$removeRule()"),e(".")],-1),Re=t("p",null,[e("The function you provide should take a single argument that contains the current value of the property, and should either return "),t("code",null,"true"),e(" to indicate that the validation rule has succeeded, or a string that will be displayed as an error message to the user.")],-1),je=t("p",null,[e("Any failing validation rules on a ViewModel will prevent that ViewModel's "),t("code",null,"$save"),e(" caller from being invoked.")],-1),Le=t("p",null,"Remove a validation rule from the ViewModel for the specified property and rule identifier.",-1),Oe=t("code",null,"$addRule",-1),Be=t("code",null,"metadata.g.ts",-1),Fe=t("p",null,[e("Returns an array of active rule functions for the specified property, or "),t("code",null,"undefined"),e(" if the property has no active validation rules.")],-1),Ge={href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator",target:"_blank",rel:"noopener noreferrer"},Ne=t("div",{class:"custom-container tip"},[t("p",{class:"custom-container-title"},"TIP"),t("p",null,[e("You can obtain an array from a generator with "),t("code",null,"Array.from(vm.$getErrors())"),e(" or "),t("code",null,"[...vm.$getErrors()]")])],-1),Ue=t("p",null,"Indicates if any properties have validation errors.",-1),We=t("h3",{id:"generated-members",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#generated-members","aria-hidden":"true"},"#"),e(" Generated Members")],-1),ze=t("h4",{id:"api-callers",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#api-callers","aria-hidden":"true"},"#"),e(" API Callers")],-1),qe=t("h4",{id:"addto-functions",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#addto-functions","aria-hidden":"true"},"#"),e(),t("code",null,"addTo*()"),e(" Functions")],-1),Ke=t("h4",{id:"many-to-many-helper-collections",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#many-to-many-helper-collections","aria-hidden":"true"},"#"),e(" Many-to-many helper collections")],-1),Xe=t("h2",{id:"listviewmodels",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#listviewmodels","aria-hidden":"true"},"#"),e(" ListViewModels")],-1),Ye=t("p",null,[e("The following members can be found on the generated ListViewModels, exported from "),t("code",null,"viewmodels.g.ts"),e(" as "),t("code",null,"*TypeName*ListViewModel"),e(".")],-1),Je=t("h3",{id:"data-properties",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#data-properties","aria-hidden":"true"},"#"),e(" Data Properties")],-1),Qe=t("code",null,"$load",-1),He=t("h3",{id:"parameters-api-callers",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#parameters-api-callers","aria-hidden":"true"},"#"),e(" Parameters & API Callers")],-1),Ze=t("code",null,"$load",-1),et=t("code",null,"$count",-1),tt=t("code",null,"$params.dataSource",-1),at=t("code",null,"$params.includes",-1),st=t("code",null,"/list",-1),ot=t("code",null,"$params",-1),nt=t("p",null,[e("Results are available in the "),t("code",null,"$items"),e(" property. The "),t("code",null,"result"),e(" property of the "),t("code",null,"$load"),e(" API Caller contains the raw results and is not recommended for use in general development - "),t("code",null,"$items"),e(" should always be preferred.")],-1),lt=t("code",null,"/count",-1),it=t("code",null,"$params",-1),rt=t("p",null,[e("The result is available in "),t("code",null,"$count.result"),e(" - this API Caller does not interact with other properties on the ListViewModel like "),t("code",null,"$pageSize"),e(" or "),t("code",null,"$pageCount"),e(".")],-1),dt=t("p",null,[e("Properties which indicate if "),t("code",null,"$page"),e(" can be decremented or incremented, respectively. "),t("code",null,"$pageCount"),e(" and "),t("code",null,"$page"),e(" are used to make this determination.")],-1),ct=t("p",null,[e("Methods that will decrement or increment "),t("code",null,"$page"),e(", respectively. Each does nothing if there is no previous or next page as returned by "),t("code",null,"$hasPreviousPage"),e(" and "),t("code",null,"$hasNextPage"),e(".")],-1),pt=t("p",null,[e("Getter/setter wrapper for "),t("code",null,"$params.page"),e(". Controls the page that will be requested on the next invocation of "),t("code",null,"$load"),e(".")],-1),ut=t("p",null,[e("Getter/setter wrapper for "),t("code",null,"$params.pageSize"),e(". Controls the page that will be requested on the next invocation of "),t("code",null,"$load"),e(".")],-1),ht=t("p",null,[e("Shorthand for "),t("code",null,"$load.pageCount"),e(" - returns the page count reported by the last successful invocation of "),t("code",null,"$load"),e(".")],-1),mt=t("h3",{id:"auto-load",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#auto-load","aria-hidden":"true"},"#"),e(" Auto-Load")],-1),ft=t("code",null,"$load",-1),yt=i(`
    type AutoLoadOptions<TThis> =
    +{ 
    +    /** Time, in milliseconds, to debounce loads for.  */
    +    wait?: number;
    +
    +    /** Additional options to pass to the third parameter of lodash's \`debounce\` function. */
    +    debounce?: DebounceSettings;
    +
    +    /** A function that will be called before loading that can return false to prevent a load. */
    +    predicate?: (viewModel: TThis) => boolean;
    +}
    +
    `,1),vt=t("p",null,"Manually turns off auto-loading of the instance.",-1),gt=t("h3",{id:"generated-members-1",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#generated-members-1","aria-hidden":"true"},"#"),e(" Generated Members")],-1),_t=t("h4",{id:"api-callers-1",tabindex:"-1"},[t("a",{class:"header-anchor",href:"#api-callers-1","aria-hidden":"true"},"#"),e(" API Callers")],-1),bt=i('

    Service ViewModels

    The following members can be found on the generated Service ViewModels, exported from viewmodels.g.ts as <ServiceName>ViewModel.

    Generated Members

    API Callers

    ',4);function wt(Dt,$t){const o=r("RouterLink"),l=r("router-link"),n=r("Prop"),d=r("ExternalLinkIcon");return u(),h("div",null,[f,c(" MARKER:summary "),t("p",null,[e("The ViewModel layer, generated as "),y,e(", exports a ViewModel class for each API-backed type in your data model ("),a(o,{to:"/modeling/model-types/entities.html"},{default:s(()=>[e("Entity Models")]),_:1}),e(", "),a(o,{to:"/modeling/model-types/dtos.html"},{default:s(()=>[e("Custom DTOs")]),_:1}),e(", and "),a(o,{to:"/modeling/model-types/services.html"},{default:s(()=>[e("Services")]),_:1}),e("). It also exports a ListViewModel type for "),a(o,{to:"/modeling/model-types/entities.html"},{default:s(()=>[e("Entity Models")]),_:1}),e(" and "),a(o,{to:"/modeling/model-types/dtos.html"},{default:s(()=>[e("Custom DTOs")]),_:1}),e(".")]),v,c(" MARKER:summary-end "),t("nav",g,[t("ul",null,[t("li",null,[a(l,{to:"#viewmodels"},{default:s(()=>[e("ViewModels")]),_:1}),t("ul",null,[t("li",null,[a(l,{to:"#model-data-properties"},{default:s(()=>[e("Model Data Properties")]),_:1})]),t("li",null,[a(l,{to:"#other-data-properties-functions"},{default:s(()=>[e("Other Data Properties & Functions")]),_:1})]),t("li",null,[a(l,{to:"#loading-parameters"},{default:s(()=>[e("Loading & Parameters")]),_:1})]),t("li",null,[a(l,{to:"#saving-and-deleting"},{default:s(()=>[e("Saving and Deleting")]),_:1})]),t("li",null,[a(l,{to:"#auto-save"},{default:s(()=>[e("Auto-save")]),_:1})]),t("li",null,[a(l,{to:"#bulk-saves"},{default:s(()=>[e("Bulk saves")]),_:1})]),t("li",null,[a(l,{to:"#rules-validation"},{default:s(()=>[e("Rules/Validation")]),_:1})]),t("li",null,[a(l,{to:"#generated-members"},{default:s(()=>[e("Generated Members")]),_:1})])])]),t("li",null,[a(l,{to:"#listviewmodels"},{default:s(()=>[e("ListViewModels")]),_:1}),t("ul",null,[t("li",null,[a(l,{to:"#data-properties"},{default:s(()=>[e("Data Properties")]),_:1})]),t("li",null,[a(l,{to:"#parameters-api-callers"},{default:s(()=>[e("Parameters & API Callers")]),_:1})]),t("li",null,[a(l,{to:"#auto-load"},{default:s(()=>[e("Auto-Load")]),_:1})]),t("li",null,[a(l,{to:"#generated-members-1"},{default:s(()=>[e("Generated Members")]),_:1})])])]),t("li",null,[a(l,{to:"#service-viewmodels"},{default:s(()=>[e("Service ViewModels")]),_:1}),t("ul",null,[t("li",null,[a(l,{to:"#generated-members-2"},{default:s(()=>[e("Generated Members")]),_:1})])])])])]),_,t("p",null,[e("The following members can be found on the generated "),a(o,{to:"/modeling/model-types/entities.html"},{default:s(()=>[e("Entity")]),_:1}),e(" and "),a(o,{to:"/modeling/model-types/dtos.html"},{default:s(()=>[e("Custom DTO")]),_:1}),e(" ViewModels, exported from "),b,e(" as "),w,e(".")]),D,t("p",null,[e("Each ViewModel class implements the corresponding interface from the "),a(o,{to:"/stacks/vue/layers/models.html"},{default:s(()=>[e("Model Layer")]),_:1}),e(", meaning that the ViewModel has a data property for each "),a(o,{to:"/modeling/model-components/properties.html"},{default:s(()=>[e("Property")]),_:1}),e(" on the model. Object-typed properties will be typed as the corresponding generated ViewModel.")]),t("p",null,[e("Changing the value of a property will automatically flag that property as dirty. See "),a(o,{to:"/stacks/vue/layers/viewmodels.html"},{default:s(()=>[e("Auto-save & Dirty Flags")]),_:1}),e(" below for information on how property dirty flags are used.")]),$,a(n,{def:"readonly $metadata: ModelType",lang:"ts"}),t("p",null,[e("The metadata object from the "),a(o,{to:"/stacks/vue/layers/metadata.html"},{default:s(()=>[e("Metadata Layer")]),_:1}),e(" layer for the type represented by the ViewModel.")]),a(n,{def:"readonly $stableId: number",lang:"ts"}),P,k,a(n,{def:"$primaryKey: string | number",lang:"ts"}),A,a(n,{def:"$display(prop?: string | Property): string",lang:"ts"}),M,a(n,{def:"$addChild(prop: string | ModelCollectionNavigationProperty, initialDirtyData?: {})",lang:"ts"}),C,S,a(n,{def:`$load: ItemApiState; +$load(id?: TKey) => ItemResultPromise;`,lang:"ts",idPrefix:"member-item"}),t("p",null,[e("An "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" for the "),x,e(" endpoint. Accepts an optional "),T,e(" argument - if not provided, the ViewModel's "),V,e(" is used instead. Uses the instance's "),I,e(" object for the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(".")]),a(n,{def:"$params: DataSourceParameters",lang:"ts",idPrefix:"member-item"}),t("p",null,[e("An object containing the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(" to be used for the "),E,e(", "),R,e(", "),j,e(", and "),L,e(" API callers.")]),a(n,{def:"$dataSource: DataSource",lang:"ts",idPrefix:"member-item"}),t("p",null,[e("Getter/setter wrapper around "),O,e(". Takes an instance of a "),a(o,{to:"/modeling/model-components/data-sources.html"},{default:s(()=>[e("Data Source")]),_:1}),e(" class "),a(o,{to:"/stacks/vue/layers/models.html"},{default:s(()=>[e("generated in the Model Layer")]),_:1}),e(".")]),a(n,{def:"$includes: string | null",lang:"ts",idPrefix:"member-item"}),t("p",null,[e("Getter/setter wrapper around "),B,e(". See "),a(o,{to:"/concepts/includes.html"},{default:s(()=>[e("Includes String")]),_:1}),e(" for more information.")]),a(n,{def:"$loadCleanData(source: {} | TModel, purgeUnsaved = false)",lang:"ts"}),F,G,N,U,a(n,{def:"$loadDirtyData(source: {} | TModel)",lang:"ts"}),W,a(n,{def:"constructor(initialDirtyData?: {} | TModel | null)",lang:"ts"}),z,q,a(n,{def:`$save: ItemApiState; +$save(overrideProps?: Partial) => ItemResultPromise;`,lang:"ts",idPrefix:"member-item"}),t("p",null,[e("An "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" for the "),K,e(" endpoint. Uses the instance's "),X,e(" object for the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(". A save operation saves only properties on the model it is called on - for deep/bulk saves, see "),Y,e(".")]),t("p",null,[e("This caller is used for both manually-triggered saves in custom code and for auto-saves. If the "),a(o,{to:"/stacks/vue/layers/viewmodels.html#rules-validation"},{default:s(()=>[e("Rules/Validation")]),_:1}),e(" report any errors when the caller is invoked, an error will be thrown.")]),J,a(n,{def:`$delete: ItemApiState; +$delete() => ItemResultPromise;`,lang:"ts",idPrefix:"member-item"}),t("p",null,[e("An "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" for the "),Q,e(" endpoint. Uses the instance's "),H,e(" object for the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(".")]),Z,a(n,{def:"$loadResponseFromSaves: boolean",lang:"ts"}),ee,a(n,{def:"$savingProps: ReadonlySet",lang:"ts"}),te,ae,a(n,{def:"$saveMode: 'surgical' | 'whole'",lang:"ts"}),se,t("div",oe,[ne,le,ie,t("p",null,[e("Save mode "),re,e(" doesn't help when multiple users are editing field X at the same time - if such a scenario is applicable to your application, you must implement "),t("a",de,[e("more advanced handling of concurrency conflicts"),a(d)]),e(".")]),t("div",ce,[pe,ue,t("p",null,[e("The "),a(o,{to:"/stacks/agnostic/dtos.html"},{default:s(()=>[e("Generated C# DTOs")]),_:1}),e(" implement the necessary logic for this; however, any "),a(o,{to:"/modeling/model-types/dtos.html"},{default:s(()=>[e("Custom DTOs")]),_:1}),e(" must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the "),a(o,{to:"/stacks/agnostic/dtos.html"},{default:s(()=>[e("Generated C# DTOs")]),_:1}),e(", or do not use surgical saves with Custom DTOs.")])]),he,me]),a(n,{def:"$getPropDirty(propName: string): boolean",lang:"ts"}),fe,a(n,{def:"$setPropDirty(propName: string, dirty: boolean = true, triggerAutoSave = true)",lang:"ts"}),ye,ve,a(n,{def:"$isDirty: boolean",lang:"ts"}),ge,_e,be,a(n,{def:`// Vue Options API +$startAutoSave(vue: Vue, options: AutoSaveOptions = {}) +  +// Vue Composition API +$useAutoSave(options: AutoSaveOptions = {})`,lang:"ts",idPrefix:"member-autosave"}),t("p",null,[e("Starts auto-saving of the instance when its savable data properties become dirty. Saves are performed with the "),we,e(),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" (documented above) and will not be performed if the ViewModel has any validation errors - see "),a(o,{to:"/stacks/vue/layers/viewmodels.html#rules-validation"},{default:s(()=>[e("Rules/Validation")]),_:1}),e(" below.")]),De,a(n,{def:"$stopAutoSave(): void",lang:"ts"}),$e,a(n,{def:"readonly $isAutoSaveEnabled: boolean",lang:"ts"}),Pe,ke,a(n,{def:`$bulkSave: ItemApiState; +$bulkSave() => ItemResultPromise;`,lang:"ts"}),Ae,Me,t("p",null,[e("If the client-side "),a(o,{to:"/stacks/vue/layers/viewmodels.html#rules-validation"},{default:s(()=>[e("Rules/Validation")]),_:1}),e(" report any errors for any of the models being saved in the operation, an error will be thrown.")]),t("p",null,[e("On the server, each affected entity is handled through the same standard mechanisms as are used by individual saves or deletes ("),a(o,{to:"/modeling/model-components/behaviors.html"},{default:s(()=>[e("Behaviors")]),_:1}),e(", "),a(o,{to:"/modeling/model-components/data-sources.html"},{default:s(()=>[e("Data Sources")]),_:1}),e(", and "),a(o,{to:"/modeling/model-components/attributes/security-attribute.html"},{default:s(()=>[e("Security Attributes")]),_:1}),e("), but with a bit of sugar on top:")]),Ce,t("p",null,[e("For the response to a bulk save, the server will load and return the root ViewModel that "),Se,e(" was called upon, using the instance's "),xe,e(" object for the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(".")]),a(n,{def:"$remove(): void",lang:"ts"}),Te,a(n,{def:"readonly $isRemoved: boolean",lang:"ts"}),Ve,Ie,a(n,{def:"$addRule(prop: string | Property, identifier: string, rule: (val: any) => true | string)",lang:"ts"}),Ee,Re,je,a(n,{def:"$removeRule(prop: string | Property, identifier: string)",lang:"ts"}),Le,t("p",null,[e("This can be used to remove either a rule that was provided by the generated "),a(o,{to:"/stacks/vue/layers/metadata.html"},{default:s(()=>[e("Metadata Layer")]),_:1}),e(", or a custom rule that was added by "),Oe,e(". Reference your generated metadata file "),Be,e(" to see any generated rules and the identifiers they use.")]),a(n,{def:"$getRules(prop: string | Property): ((val: any) => string | true)[]",lang:"ts"}),Fe,a(n,{def:"$getErrors(prop?: string | Property): Generator",lang:"ts"}),t("p",null,[e("Returns a "),t("a",Ge,[e("generator"),a(d)]),e(" that provides all error messages for either a specific property (if provided) or the entire model (if no prop argument is provided).")]),Ne,a(n,{def:"readonly $hasError: boolean",lang:"ts"}),Ue,We,ze,t("p",null,[e("For each of the instance "),a(o,{to:"/modeling/model-components/methods.html"},{default:s(()=>[e("Methods")]),_:1}),e(" of the type, an "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" will be generated.")]),qe,t("p",null,[e("For each "),a(o,{to:"/modeling/model-components/properties.html"},{default:s(()=>[e("collection navigation property")]),_:1}),e(", a method is generated that will create a new instance of the ViewModel for the collected type, add it to the collection, and then return the new object.")]),Ke,t("p",null,[e("For each "),a(o,{to:"/modeling/model-components/properties.html"},{default:s(()=>[e("collection navigation property")]),_:1}),e(" annotated with "),a(o,{to:"/modeling/model-components/attributes/many-to-many.html"},{default:s(()=>[e("[ManyToMany]")]),_:1}),e(", a getter-only property is generated that returns a collection of the object on the far side of the many-to-many relationship. Nulls are filtered from this collection.")]),Xe,Ye,Je,a(n,{def:"readonly $items: T[]",lang:"ts"}),t("p",null,[e("Collection holding the results of the last successful invocation of the "),Qe,e(),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(".")]),He,a(n,{def:"$params: ListParameters",lang:"ts",idPrefix:"member-list"}),t("p",null,[e("An object containing the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(" to be used for the "),Ze,e(" and "),et,e(" API callers.")]),a(n,{def:"$dataSource: DataSource",lang:"ts",idPrefix:"member-list"}),t("p",null,[e("Getter/setter wrapper around "),tt,e(". Takes an instance of a "),a(o,{to:"/modeling/model-components/data-sources.html"},{default:s(()=>[e("Data Source")]),_:1}),e(" class "),a(o,{to:"/stacks/vue/layers/models.html"},{default:s(()=>[e("generated in the Model Layer")]),_:1}),e(".")]),a(n,{def:"$includes: string | null",lang:"ts",idPrefix:"member-list"}),t("p",null,[e("Getter/setter wrapper around "),at,e(". See "),a(o,{to:"/concepts/includes.html"},{default:s(()=>[e("Includes String")]),_:1}),e(" for more information.")]),a(n,{def:`$load: ListApiState; +$load() => ListResultPromise`,lang:"ts",idPrefix:"member-list"}),t("p",null,[e("An "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" for the "),st,e(" endpoint. Uses the instance's "),ot,e(" object for the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(".")]),nt,a(n,{def:`$count: ItemApiState; +$count() => ItemResultPromise`,lang:"ts"}),t("p",null,[e("An "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" for the "),lt,e(" endpoint. Uses the instance's "),it,e(" object for the "),a(o,{to:"/modeling/model-components/data-sources.html#standard-parameters"},{default:s(()=>[e("Standard Parameters")]),_:1}),e(".")]),rt,a(n,{def:`readonly $hasPreviousPage: boolean +readonly $hasNextPage: boolean`,lang:"ts"}),dt,a(n,{def:`$previousPage(): void +$nextPage(): void`,lang:"ts"}),ct,a(n,{def:"$page: number",lang:"ts"}),pt,a(n,{def:"$pageSize: number",lang:"ts"}),ut,a(n,{def:"readonly $pageCount: number",lang:"ts"}),ht,mt,a(n,{def:`// Vue Options API +$startAutoLoad(vue: Vue, options: AutoLoadOptions = {}) +  +// Vue Composition API +$useAutoLoad(options: AutoLoadOptions = {})`,lang:"ts",idPrefix:"member-autoLoad"}),t("p",null,[e("Starts auto-loading of the list as changes to its parameters occur. Loads are performed with the "),ft,e(),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(".")]),yt,a(n,{def:"$stopAutoLoad()",lang:"ts"}),vt,gt,_t,t("p",null,[e("For each of the static "),a(o,{to:"/modeling/model-components/methods.html"},{default:s(()=>[e("Methods")]),_:1}),e(" on the type, an "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" will be created.")]),bt,t("p",null,[e("For each method of the "),a(o,{to:"/modeling/model-types/services.html"},{default:s(()=>[e("Service")]),_:1}),e(", an "),a(o,{to:"/stacks/vue/layers/api-clients.html#api-callers"},{default:s(()=>[e("API Caller")]),_:1}),e(" will be created.")])])}const kt=p(m,[["render",wt],["__file","viewmodels.html.vue"]]);export{kt as default}; diff --git a/assets/vue2-to-vue3.html.6bf93dcd.js b/assets/vue2-to-vue3.html.6bf93dcd.js new file mode 100644 index 000000000..1311b5432 --- /dev/null +++ b/assets/vue2-to-vue3.html.6bf93dcd.js @@ -0,0 +1 @@ +const e=JSON.parse('{"key":"v-6ffb6087","path":"/stacks/vue/vue2-to-vue3.html","title":"Vue 2 to Vue 3","lang":"en-US","frontmatter":{"pageClass":"wide-page"},"excerpt":"","headers":[{"level":2,"title":"Coalesce Upgrade Steps","slug":"coalesce-upgrade-steps","link":"#coalesce-upgrade-steps","children":[]},{"level":2,"title":"From Class Components to + Include Tree | Coalesce + + + + +

    Include Tree

    When Coalesce maps from the your POCO objects that are returned from EF Core queries, it will follow a structure called an IncludeTree to determine what relationships to follow and how deep to go in re-creating that structure in the mapped DTOs.

    Purpose

    Without an IncludeTree present, Coalesce will map the entire object graph that is reachable from the root object. This can often spiral out of control if there aren't any rules defining how far to go while turning this graph into a tree.

    For example, suppose you had the following model with a many-to-many relationship (key properties omitted for brevity):

    public class Employee
    +{
    +    [ManyToMany("Projects")]
    +    public ICollection<EmployeeProject> EmployeeProjects { get; set; }
    +            
    +    public static IQueryable<Employee> WithProjectsAndMembers(AppDbContext db, ClaimsPrincipal user)
    +    {
    +        // Load all projects of an employee, as well as all members of those projects.
    +        return db.Employees
    +            .Include(e => e.EmployeeProjects)
    +                .ThenInclude(ep => ep.Project.EmployeeProjects)
    +                .ThenInclude(ep => ep.Employee);
    +    }
    +}
    +
    +public class Project
    +{
    +    [ManyToMany("Employees")]
    +    public ICollection<EmployeeProject> EmployeeProjects { get; set; }
    +}
    +
    +public class EmployeeProject
    +{
    +    public Employee Employee { get; set; }
    +    public Project Project { get; set; }
    +}
    +

    Now, imagine that you have five employees and five projects, with every employee being a member of every project (i.e. there are 25 EmployeeProject rows).

    Your client code makes a call to the Coalesce-generated API to load Employee #1 using the custom data source:

    import { Employee } from '@/viewmodels.g'
    +import { EmployeeViewModel } from '@/viewmodels.g'
    +
    +var employee = new EmployeeViewModel();
    +employee.$dataSource = new Employee.DataSources.WithProjectsAndMembers();
    +employee.$load(1);
    +

    If you're already familiar with the fact that an IncludeTree is implicitly created in this scenario, then imagine for a moment that this is not the case (if you're not familiar with this fact, then keep reading!).

    After Coalesce has called your Data Sources and evaluated the EF IQueryable returned, there are now 35 objects loaded into the current DbContext being used to handle this request - the 5 employees, 5 projects, and 25 relationships.

    To map these objects to DTOs, we start with the root (employee #1) and expand outward from there until the entire object graph has been faithfully re-created with DTO objects, including all navigation properties.

    The root DTO object (employee #1) then eventually is passed to the JSON serializer by ASP.NET Core to formulate the response to the request. As the object is serialized to JSON, the only objects that are not serialized are those that were already serialized as an ancestor of itself. What this ultimately means is that the structure of the serialized JSON with our example scenario ends up following a pattern like this (the vast majority of items have been omitted):

    Employee#1
    +    EmployeeProject#1
    +        Project#1
    +            EmployeeProject#6
    +                Employee#2
    +                    EmployeeProject#7
    +                        Project#2
    +                            ... continues down through all remaining employees and projects.
    +                    ...
    +            EmployeeProject#11
    +                Employee#3
    +            ...
    +    EmployeeProject#2
    +        Project#2
    +    ...
    +

    See how the structure includes the EmployeeProjects of Employee#2? We didn't write our custom data source calls to .Include in such a way that indicated that we wanted the root employee, their projects, the employees of those projects, and then the projects of those employees. But, because the JSON serializer blindly follows the object graph, that's what gets serialized. It turns out that the depth of the tree increases on the order of O(n^2), and the total size increases on the order of Ω(n!).

    This is where IncludeTree comes in. When you use a custom data source like we did above, Coalesce automatically captures the structure of the calls to .Include and .ThenInclude, and uses this to perform trimming during creation of the DTO objects.

    With an IncludeTree in place, our new serialized structure looks like this:

    Employee#1
    +    EmployeeProject#1
    +        Project#1
    +            EmployeeProject#6
    +                Employee#2
    +            EmployeeProject#11
    +                Employee#3
    +            ...
    +    EmployeeProject#2
    +        Project#2
    +    ...
    +

    No more extra data trailing off the end of the projects' employees!

    Usage

    Custom Data Sources

    In most cases, you don't have to worry about creating an IncludeTree. When using the Standard Data Source (or a derivative), the structure of the .Include and .ThenInclude calls will be captured automatically and be turned into an IncludeTree.

    However, there are sometimes cases where you perform complex loading in these methods that involves loading data into the current DbContext outside of the IQueryable that is returned from the method. The most common situation for this is needing to conditionally load related data - for example, load all children of an object where the child has a certain value of a Status property.

    In these cases, Coalesce provides a pair of extension methods, .IncludedSeparately and .ThenIncluded, that can be used to merge in the structure of the data that was loaded separately from the main IQueryable.

    For example:

    public override IQueryable<Employee> GetQuery()
    +{
    +    // Load all projects that are complete, and their members, into the db context.
    +    Db.Projects
    +        .Include(p => p.EmployeeProjects).ThenInclude(ep => ep.Employee)
    +        .Where(p => p.Status == ProjectStatus.Complete)
    +        .Load();
    +
    +    // Return an employee query, and notify Coalesce that we loaded the projects in a different query.
    +    return Db.Employees
    +        .IncludedSeparately(e => e.EmployeeProjects)
    +        .ThenIncluded(ep => ep.Project.EmployeeProjects)
    +        .ThenIncluded(ep => ep.Employee);
    +}
    +

    You can also override the GetIncludeTree method of the Standard Data Source to achieve the same result:

    public override IncludeTree GetIncludeTree(IQueryable<T> query, IDataSourceParameters parameters) => Db
    +    .Employees
    +    .IncludedSeparately(e => e.EmployeeProjects)
    +    .ThenIncluded(ep => ep.Project.EmployeeProjects)
    +    .ThenIncluded(ep => ep.Employee)
    +    .GetIncludeTree(); 
    +

    Model Methods

    If you have custom methods that return object data, you may also want to control the structure of the returned data when it is serialized. Fortunately, you can also use IncludeTree in these situations. Without an IncludeTree, the entire object graph is traversed and serialized without limit.

    TIP

    An IncludeTree can be obtained from any IQueryable by calling the GetIncludeTree extension method (using IntelliTect.Coalesce.Helpers.IncludeTree).

    In situations where your root object isn't on your DbContext (see External Types), you can use Enumerable.Empty<MyNonDbClass>().AsQueryable() to get an IQueryable to start from. When you do this, you must use IncludedSeparately - the regular EF Include method won't work without a DbSet.

    See the following two techniques for returning an IncludeTree from a custom method:

    ItemResult.IncludeTree

    The easiest and most versatile way to return an IncludeTree from a custom method is to make that method return an ItemResult<T>, and then set the IncludeTree property of the ItemResult object. For example:

    public class Employee
    +{
    +    public async Task<ItemResult<ICollection<Employee>>> GetChainOfCommand(AppDbContext db)
    +    {
    +        IQueryable<Employee> query = db.Employees
    +            .Include(e => e.Supervisor);
    +
    +        var ret = new List<Employee>();
    +        var current = this;
    +        while (current.Supervisor != null)
    +        {
    +            ret.Push(current);
    +            current = await query.FirstOrDefaultAsync(e => e.EmployeeId == current.SupervisorId);
    +        }
    +
    +        return new(ret, includeTree: query.GetIncludeTree());
    +    }
    +}
    +

    Out Parameter

    To tell Coalesce about the structure of the data returned from a model method, you can also add out IncludeTree includeTree to the signature of the method. Inside your method, set includeTree to an instance of an IncludeTree. However, this approach cannot be used on async methods, since out parameters are not allowed on async methods in C#. For example:

    public class Employee
    +{
    +    public ICollection<Employee> GetChainOfCommand(AppDbContext db, out IncludeTree includeTree)
    +    {
    +        IQueryable<Employee> query = db.Employees
    +            .Include(e => e.Supervisor);
    +
    +        var ret = new List<Employee>();
    +        var current = this;
    +        while (current.Supervisor != null)
    +        {
    +            ret.Push(current);
    +            current = query.FirstOrDefault(e => e.EmployeeId == current.SupervisorId);
    +        }
    +
    +        includeTree = query.GetIncludeTree();
    +
    +        return ret;
    +    }
    +}
    +

    External Type Caveats

    One important point remains regarding IncludeTree - it is not used to control the serialization of objects which are not mapped to the database, known as External Types. External Types are always put into the DTOs when encountered (unless otherwise prevented by [DtoIncludes] & [DtoExcludes] or Security Attributes), with the assumption that because these objects are created by you (as opposed to Entity Framework), you are responsible for preventing any undesired circular references.

    By not filtering unmapped properties, you as the developer don't need to account for them in every place throughout your application where they appear - instead, they 'just work' and show up on the client as expected.

    Note also that this statement does not apply to database-mapped objects that hang off of unmapped objects - any time a database-mapped object appears, it will be controlled by your include tree. If no include tree is present (because nothing was specified for the unmapped property), these mapped objects hanging off of unmapped objects will be serialized freely and with all circular references, unless you include some calls to .IncludedSeparately(m => m.MyUnmappedProperty.MyMappedProperty) to limit those objects down.

    + + + diff --git a/concepts/includes.html b/concepts/includes.html new file mode 100644 index 000000000..41f2f4d77 --- /dev/null +++ b/concepts/includes.html @@ -0,0 +1,88 @@ + + + + + + + + + Includes String | Coalesce + + + + +

    Includes String

    Coalesce provides a number of extension points for loading & serialization which make use of a concept called an "includes string" (also referred to as "include string" or just "includes").

    Includes String

    The includes string is simply a string which can be set to any arbitrary value. It is passed from the client to the server in order to control data loading and serialization. It can be set on both the TypeScript ViewModels and the ListViewModels.

    import { PersonViewModel, PersonListViewModel } from '@/viewmodels.g'
    +
    +var person = new PersonViewModel();
    +person.$includes = "details";
    +
    +var personList = new PersonListViewModel();
    +personList.$includes = "details";
    +

    The default value (i.e. no action) is the empty string.

    Special Values

    There are a few values of includes that are either set by default in the auto-generated views, or otherwise have special meaning:

    ValueDescription
    'none'Setting includes to none suppresses the Default Loading Behavior provided by the Standard Data Source - The resulting data will be the requested object (or list of objects) and nothing more.
    'admin-list'Used when loading a list of objects in the Vue admin list page.
    'admin-editor'Used when loading an object in the Vue admin editor component.
    'Editor'Used when loading an object in the generated Knockout CreateEdit views.
    '<ModelName>ListGen'Used when loading a list of objects in the generated Knockout Table and Cards views. For example, PersonListGen

    DtoIncludes & DtoExcludes

    Main document: [DtoIncludes] & [DtoExcludes].

    There are two C# attributes, DtoIncludes and DtoExcludes, that can be used to annotate your data model in order to control what data gets put into the DTOs and ultimately serialized to JSON and sent out to the client.

    When the database entries are returned to the client they will be trimmed based on the requested includes string and the values in DtoExcludes and DtoIncludes.

    DANGER

    These attributes are not security attributes - consumers of your application's API can set the includes string to any value when making a request.

    Do not use them to keep certain data private - use the Security Attributes family of attributes for that.

    It is important to note that the value of the includes string will match against these attributes on any of your models that appears in the object graph being mapped to DTOs - it is not limited only to the model type of the root object.

    Important

    DtoIncludes does not ensure that specific data will be loaded from the database. It only permits what is already loaded into the current EF DbContext to be returned from the API. See Data Sources to learn how to control what data gets loaded from the database.

    Example Usage

    Server code:

    public class Person
    +{
    +    // Don't include CreatedBy when editing - will be included for all other views
    +    [DtoExcludes("Editor")]
    +    public AppUser CreatedBy { get; set; }
    +
    +    // Only include the Person's Department when `includes == "details"` on the TypeScript ViewModel.
    +    [DtoIncludes("details")]
    +    public Department Department { get; set; }
    +
    +    // LastName will be included in all views
    +    public string LastName { get; set; }
    +}
    +
    +public class Department
    +{
    +    [DtoIncludes("details")]
    +    public ICollection<Person> People { get; set; }
    +}
    +

    Client code:

    import { PersonListViewModel } from '@/viewmodels.g'
    +
    +const personList = new PersonListViewModel();
    +personList.$includes = "Editor";
    +await personList.$load();
    +// Objects in personList.$items will not contain CreatedBy nor Department objects.
    +
    +const personList2 = new PersonListViewModel();
    +personList2.$includes = "details";
    +await personList.$load();
    +// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. 
    +// Department will be allowed to include its other Person objects.
    +

    Properties

    // Also settable via constructor parameter #1
    +public string ContentViews { get; set; }

    A comma-delimited list of values of includes on which to operate.

    For DtoIncludes, this will be the values of includes for which this property will be allowed to be serialized and sent to the client.

    For DtoExcludes, this will be the values of includes for which this property will not be serialized and sent to the client.

    + + + diff --git a/history.html b/history.html new file mode 100644 index 000000000..5133d82c9 --- /dev/null +++ b/history.html @@ -0,0 +1,33 @@ + + + + + + + + + Welcome to Coalesce's documentation! | Coalesce + + + + +

    Welcome to Coalesce's documentation!

    Coalesce is a framework based on ASP.NET Core that makes rapidly building awesome websites much easier. A project that would take a 3 months to complete now takes 1 month. We built this because we got tired of writing all the boiler plate code that is necessary to make amazing sites.

    It does this by allowing developers to focus on the creative aspects of the solution. The more mundane parts are generated automatically. This means that you get to focus on data modeling, business logic and front-end development. Coalesce does the plumbing.

    Here is a typical workflow

    1. Build an EF Core data model with business logic
    2. Coalesce generates controllers, TypeScript view models, API and view model documentation, and admin pages/examples
    3. Build an interactive and intuitive user experience
    4. Rinse and repeat

    Core Features

    • Built on the latest Microsoft ASP.NET Core
    • Easy to learn
    • TypeScript from the ground up
    • Flexibility to use MVC patterns as required
    • Admin pages for all your models are build automatically and include features like searching, sorting, and paging
    • Robust documentation for the framework
    • Automatically generated documentation for the API layer and TypeScript view models
    • Feature rich TypeScript view models that can be easily extended
    • Many extension points for customizations
    • Abstraction that doesn't require you know how everything works
    • Security and data trimming by role is built in
    • Flexibility about which data to return to the client
    • Open source

    Is Coalesce for Everyone and Every Project?

    Coalesce was designed to create line-of-business applications. It provides a more customizable and maintainable alternative to off the shelf customizable products like SharePoint and Sales Force.

    You should consider using Coalesce if your project:

    • Is small to medium size (1-200 classes)
    • Requires an interactive user experience
    • Has data entry requirements, especially forms, tables, etc.
    • Needs to get started quickly with functional prototypes that can become production software

    Design Decisions and Limitations

    Coalesce is specifically designed to meet the needs of web developers. However, there are lots of ways to do this. We have made a set of decisions which we believe makes for a great development experience

    • ASP.NET Core: there is no intent to back port this to an earlier version
    • EF Core for the object relational mapper
    • Currently uses the full framework because .NET Core doesn't supported the required functionality, yet
    • Knockout for client-side data binding
    • Business logic most easily lives in the model classes
    • Coalesce is designed for relational databases. This might change in the future, but not until we have a compelling use case.

    How Does it Work?

    After you create your classes and the EF data context, Coalesce uses this information to generate code. When the Coalesce CLI (command line interface) is run, the following things happen:

    1. The model is validated to ensure that all the Coalesce specific requirements are met. This includes things like ensuring that all classes have a primary key assigned, validating that linked child objects have a key to their parent, etc. If issues are found, generation stops and the errors are displayed with advise to fix the issues.
    2. The core files needed for Coalesce are copied to the target project. This includes TypeScript base classes, customizable templates, and other files for extension points. Each file is copied twice, once as a file that can be modified in the project and once as an original file. This ensures that if any changes are made by the user these files Coalesce will not overwrite the your changes.
    3. The API controllers are generated. One is generated for each object. This includes methods that get a list of items, get a specific item, save an item, etc.
    4. The TypeScript view models are created. There are two view models for each object. One is a list view model which allows for getting an displaying lists of a type of object. This includes full functionality to sort, filter, search, page, etc. Additionally, a view model that represents the individual object is also created. This has all the properties and methods of the server side object. This is basically a client-side proxy object for representing and manipulating the object on the server side. These objects seamlessly use the API controllers to interact with the server.
    5. Next, the View controller are created. One is create for each model class and provide a tabular view, a card view (for mobile), an editor, and documentation.
    6. Finally, the CSHTML views for the controller are created. These are the actual CSHTML for the above controllers. These not only provide administrative view and editing features, but also serve as an example of how to use the framework

    General Guidance

    Here are a few things we have found helpful when using Coalesce

    • Learn and embrace the Coalesce paradigm and work with it rather than trying to do things another way.
    • Following what we refer to as the 'well worn path' is very helpful. Try to stick to standard ways to do things rather than trying to use esoteric features.
    • Keep your models as consistent and straightforward as possible. Use relational modeling best practices.
    • Remember that public methods on your class models are added to the client side view models and this makes calling business logic from the client really easy.
    • Don't be afraid to fall back to building parts of your site using traditional methods. Coalesce isn't right for everything. But, honestly, we have only done this a few times, like 3.

    The Story

    Why Coalesce

    In 2014 several developers from IntelliTect got together to talk about our craft. There were lots of different backgrounds, but recently we had all been writing web code in C#. We discussed things we enjoyed and things we dreaded. There was an underlying commitment to providing customers with great sites at a reasonable cost. However, those things often seemed at odds because of the complexity of web development.

    The Problem

    For example, writing AJAX drop down lists with type ahead takes quite a bit of plumbing. Layer onto this the need for view models that allow for validation and saving as the user moves from field to field. We absolutely want want to deliver visually pleasing sites with complex UI paradigms. However, all this excellence adds up: complex view models, complex APIs, data binding, ugh.

    Then there is that sinking feeling when you have to add another class to the project knowing that you are going to need to create all this yet again and you consider taking short cuts. Will there really be more than about 20 items in this table, maybe we don't need paging. Inevitably, the customer asks for admin screens. We consider giving them SQL Server Management Console and then consider using the built in ASP.NET list and editor pages. Better sense wins out and we end up spending two weeks building slick admin pages with paging, searching, sorting, etc.

    The Path to the Solution

    That evening we starting talking about the things we loved to do:

    • Data modeling
    • Figuring out and writing business logic
    • Working with customers
    • Making cool user interfaces
    • Creating something new and awesome

    We also lists things that we didn't enjoy

    • Writing the same controller again
    • Creating a view model for a class that is similar but different from another one in the project
    • Putting sorting and paging on every admin page
    • Basically doing anything that feels repetitive or boilerplate

    Over the next few months we talked about this issue, but couldn't find the right abstraction. We talked about other solutions that solve parts of the problem and considered putting together something from several pieces. Nothing felt unified and we ended up with leaky abstractions. We needed some way to divide the problem so that we could build the fun stuff and have something generate the boring stuff. This solution needed to be robust enough to satisfy our customer's needs and also be of use to developers without their needing to know the inner workings of the system.

    Our Solution

    What if we could build the models and business logic and have a tool build everything except the UI? There are great tools like Entity Framework for modeling and good tooling for minimizing duplicate code in user interfaces. And so Coalesce was born, a tool that would bring together the backend and front end development. '

    Coalesce takes Entity Framework Core models and builds controllers, TypeScript view models, and admin pages automatically. These are built in a general way so that they can be applied to many different scenarios. There will always be pages that need to be written by hand and we intentionally don't support many edge cases in order to keep things simple. There is nothing wrong with building something by hand.

    How has it Worked?

    We have been using Coalesce for many of our web projects with great success. Typically, a project is taking about 1/3 the time it was taking before once developers ramp up. The ramp up on Coalesce has typically been a couple of days. We realized that in order for Coalesce to be useful it need to be intuitive to use and easy to understand. We have intentionally used simple paradigms to minimize the learning curve. There are complex bits, but hopefully, those are well hidden and documented as needed.

    + + + diff --git a/index.html b/index.html new file mode 100644 index 000000000..5c23d9a3f --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + + + + + + Coalesce Documentation | Coalesce + + + + +

    Coalesce

    Designed to help you quickly build amazing web applications, Coalesce is a rapid-development code generation framework, created by IntelliTectopen in new window and built on top of:

    What do I do?

    You are responsible for the interesting parts of your application:

    • Data Model
    • Business Logic
    • External Integrations
    • Page Content
    • Site Design
    • Custom Scripting

    What is done for me?

    Coalesce builds the part of your application that are mundane and monotonous to build:

    • Client side TypeScript ViewModels that mirror your data model for both lists and individual objects. Utilize these to rapidly build out your application's various pages.
    • APIs to interact with your models via endpoints like List, Get, Save, and more.
    • Out-of-the-box Vue Components for common controls like dates, selecting objects via drop downs, enums, etc. Dropdowns support searching and paging automatically.
    • A complete set of admin pages are provided, allowing you to read, create, edit, and delete data straight away without writing any additional code.

    Getting Started

    To get started with Coalesce, check out Getting Started with Vue.

    While Knockout.js is still supported by Coalesce, it is a deprecated option and not recommended for new projects. If you do still want to choose Knockout, click here.

    + + + diff --git a/modeling/model-components/attributes.html b/modeling/model-components/attributes.html new file mode 100644 index 000000000..33e8a9210 --- /dev/null +++ b/modeling/model-components/attributes.html @@ -0,0 +1,33 @@ + + + + + + + + + Attributes | Coalesce + + + + +

    Attributes

    Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from System.ComponentModel.DataAnnotations.

    Coalesce Attributes

    Browse the list in the sidebar to learn about the attributes that Coalesce provides that can be used to decorate your models.

    ComponentModel Attributes

    Coalesce also supports a number of the built-in System.ComponentModel.DataAnnotations attributes and will use these to shape the generated code.

    [Display]

    The displayed name and description of a property, as well as the order in which it appears in generated views, can be set via the [Display] attribute. By default, properties will be displayed in the order in which they are defined in their class.

    [DisplayName]

    The displayed name of a property can also be set via the [DisplayName] attribute.

    [Required]

    Properties with [Required] will generate client validation and server validation rules.

    [Range]

    Properties with [Range] will generate client validation and server validation rules.

    [MinLength]

    Properties with [MinLength] will generate client validation and server validation rules.

    [MaxLength]

    Properties with [MaxLength] will generate client validation and server validation rules.

    [DataType]

    Some values of DataType when provided to DataTypeAttribute on a string property will alter the behavior of the Vue Components. See c-display and See c-display for details.

    [ForeignKey]

    Normally, Coalesce figures out which properties are foreign keys, but if you don't use standard EF naming conventions then you'll need to annotate with [ForeignKey] to help out both EF and Coalesce. See the Entity Framework Relationshipsopen in new window documentation for more.

    [InverseProperty]

    Sometimes, Coalesce (and EF, too) can have trouble figuring out what the foreign key is supposed to be for a collection navigation property. See the Entity Framework Relationshipsopen in new window documentation for details on how and why to use [InverseProperty].

    [DatabaseGenerated]

    Primary Keys with [DatabaseGenerated(DatabaseGeneratedOption.None)] will be settable on the client and will be appropriately handled by the Standard Behaviors on the server. Unsupported on the Knockout front-end stack.

    [NotMapped]

    Model properties that aren't mapped to the database should be marked with [NotMapped] so that Coalesce doesn't try to load them from the database when searching or carrying out the Default Loading Behavior.

    + + + diff --git a/modeling/model-components/attributes/client-validation.html b/modeling/model-components/attributes/client-validation.html new file mode 100644 index 000000000..3c38f7d66 --- /dev/null +++ b/modeling/model-components/attributes/client-validation.html @@ -0,0 +1,68 @@ + + + + + + + + + [ClientValidation] | Coalesce + + + + +

    [ClientValidation]

    The [IntelliTect.Coalesce.DataAnnotations.ClientValidation] attribute is used to control the behavior of client-side model validation and to add additional client-only validation parameters. Database validation is available via standard System.ComponentModel.DataAnnotations annotations.

    These propagate to the client as validations in TypeScript via generated Metadata and ViewModel rules (for Vue) or Knockout-Validationopen in new window rules (for Knockout). For both stacks, any failing validation rules prevent saves from going to the server.

    WARNING

    This attribute controls client-side validation only. To perform server-side validation, create a custom Behaviors class for your types.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    [ClientValidation(IsRequired = true, AllowSave = true)]
    +    public string FirstName { get; set; }
    +
    +    [ClientValidation(IsRequired = true, AllowSave = false, MinLength = 1, MaxLength = 100)]
    +    public string LastName { get; set; }
    +}
    +

    Properties

    Behavioral Properties

    public bool AllowSave { get; set; } // Knockout Only

    If set to true, any client validation errors on the property will not prevent saving on the client. This includes all client-side validation, including null-checking for required foreign keys and other validations that are implicit. This also includes other explicit validation from System.ComponentModel.DataAnnotations annotations.

    Instead, validation errors will be treated only as warnings, and will be available through the warnings: KnockoutValidationErrors property on the TypeScript ViewModel.

    Note

    Use AllowSave = true to allow partially complete data to still be saved, protecting your user from data loss upon navigation while still hinting to them that they are not done filling out data.

    public string ErrorMessage { get; set; }

    Set an error message to be used if any client validations fail

    Validation Rule Properties

    In addition to the following properties, you also customize validation on a per-instance basis of the ViewModels using the Rules/Validation methods.

    public bool IsRequired { get; set; }
    +public double MinValue { get; set; } = double.MaxValue;
    +public double MaxValue { get; set; } = double.MinValue;
    +public double MinLength { get; set; } = double.MaxValue;
    +public double MaxLength { get; set; } = double.MinValue;
    +public string Pattern { get; set; }
    +public bool IsEmail { get; set; }
    +public bool IsPhoneUs { get; set; }
    +
    + + + diff --git a/modeling/model-components/attributes/coalesce.html b/modeling/model-components/attributes/coalesce.html new file mode 100644 index 000000000..b07d23d5e --- /dev/null +++ b/modeling/model-components/attributes/coalesce.html @@ -0,0 +1,33 @@ + + + + + + + + + [Coalesce] | Coalesce + + + + +

    [Coalesce]

    Used to mark a type or member for generation by Coalesce.

    Some types and members will be implicitly included in generation - for example, all types represented by a DbSet<T> on a DbContext that has a [Coalesce] attribute will automatically be included. Properties on these types will also be generated for unless explicitly excluded, since this is by far the most common usage scenario in Coalesce.

    On the other hand, Methods on these types will not have endpoints generated unless they are explicitly annotated with [Coalesce] to avoid accidentally exposing methods that were perhaps not intended to be exposed.

    The documentation pages for types and members that require/accept this attribute will state as such. An exhaustive list of all valid targets for [Coalesce] will not be found on this page.

    + + + diff --git a/modeling/model-components/attributes/controller-action.html b/modeling/model-components/attributes/controller-action.html new file mode 100644 index 000000000..653aaa37e --- /dev/null +++ b/modeling/model-components/attributes/controller-action.html @@ -0,0 +1,61 @@ + + + + + + + + + [ControllerAction] | Coalesce + + + + +

    [ControllerAction]

    Specifies how a custom method is exposed via HTTP. Can be used to customize the HTTP method/verb for the method, as well as caching behavior.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string LastName { get; set; }
    +
    +    public string PictureHash { get; set; }
    +
    +    [Coalesce]
    +    [ControllerAction(Method = HttpMethod.Get)]
    +    public static long PersonCount(AppDbContext db, string lastNameStartsWith = "")
    +    {
    +        return db.People.Count(f => f.LastName.StartsWith(lastNameStartsWith));
    +    }
    +
    +    [Coalesce]
    +    [ControllerAction(HttpMethod.Get, VaryByProperty = nameof(PictureHash))]
    +    public IFile GetPicture(AppDbContext db)
    +    {
    +        return new IntelliTect.Coalesce.Models.File(db.PersonPictures
    +            .Where(x => x.PersonId == this.PersonId)
    +            .Select(x => x.Content)
    +        )
    +        {
    +            ContentType = "image/jpg",
    +        };
    +    }
    +}
    +

    Properties

    // Also settable via constructor parameter #1
    +public HttpMethod Method { get; set; } = HttpMethod.Post;

    The HTTP method to use on the generated API Controller.

    Enum values are:

    • HttpMethod.Post Use the POST method.
    • HttpMethod.Get Use the GET method.
    • HttpMethod.Put Use the PUT method.
    • HttpMethod.Delete Use the DELETE method.
    • HttpMethod.Patch Use the PATCH method.

    public string VaryByProperty { get; set; }

    For HTTP GET model instance methods, if VaryByProperty is set to the name of a property on the parent model class, ETag headersopen in new window based on the value of this property will be used to implement caching. If the client provides a matching If-None-Match Header with the request, the method will not be invoked and HTTP Status `304 Not Modified`` will be returned.

    Additionally, if the VaryByProperty is set to a client-exposed property, the value of the property will be included in the query string when performing API calls to invoke the method. If the query string value matches the current value on the model, a long-term Cache-Control header will be set on the response, allowing the client to avoid making future invocations to the same method while the value of the VaryByProperty remains the same.

    + + + diff --git a/modeling/model-components/attributes/controller.html b/modeling/model-components/attributes/controller.html new file mode 100644 index 000000000..65a3072a9 --- /dev/null +++ b/modeling/model-components/attributes/controller.html @@ -0,0 +1,40 @@ + + + + + + + + + [Controller] | Coalesce + + + + +

    [Controller]

    Allows for control over the generated MVC Controllers.

    Currently only controls over the API controllers are present, but additional properties may be added in the future.

    This attribute may be placed on any type from which an API controller is generated, including Entity Models, Custom DTOs, and Services.

    Example Usage

    [Controller(ApiRouted = false, ApiControllerSuffix = "Gen", ApiActionsProtected = true)]
    +public class Person
    +{
    +    public int PersonId { get; set; }
    +    
    +    ...
    +}
    +

    Properties

    public bool ApiRouted { get; set; } = true;

    Determines whether or not a [Route] annotation will be placed on the generated API controller. Set to false to prevent emission of the [Route] attribute.

    Use cases include:

    • Defining your routes through IRouteBuilder in Startup.cs instead
    • Preventing API controllers from being exposed by default.
    • Routing to your own custom controller that inherits from the generated API controller in order to implement more granular or complex authorization logic.

    public string ApiControllerName { get; set; } = null;

    If set, will determine the name of the generated API controller.

    Takes precedence over the value of ApiControllerSuffix.

    public string ApiControllerSuffix { get; set; } = null;

    If set, will be appended to the default name of the API controller generated for this model.

    Will be overridden by the value of ApiControllerName if it is set.

    public bool ApiActionsProtected { get; set; } = false;

    If true, actions on the generated API controller will have an access modifier of protected instead of public.

    In order to consume the generated API controller, you must inherit from the generated controller and override each desired generated action method via hiding (i.e. use public new ..., not public override ...).

    Note

    If you inherit from the generated API controllers and override their methods without setting ApiActionsProtected = true, all non-overridden actions from the generated controller will still be exposed as normal.

    + + + diff --git a/modeling/model-components/attributes/create-controller.html b/modeling/model-components/attributes/create-controller.html new file mode 100644 index 000000000..1c9f700f3 --- /dev/null +++ b/modeling/model-components/attributes/create-controller.html @@ -0,0 +1,42 @@ + + + + + + + + + [CreateController] | Coalesce + + + + +

    [CreateController]

    By default an API and View controller are both created. This allows for suppressing the creation of either or both of these.

    Example Usage

    [CreateController(view: false, api: true)]
    +public class Person
    +{
    +    public int PersonId { get; set; }
    +    
    +    ...
    +}
    +

    Properties

    // Also settable via constructor parameter #1
    +public bool WillCreateView { get; set; } = true

    // Also settable via constructor parameter #2
    +public bool WillCreateApi { get; set; } = true

    + + + diff --git a/modeling/model-components/attributes/date-type.html b/modeling/model-components/attributes/date-type.html new file mode 100644 index 000000000..939c7b3e0 --- /dev/null +++ b/modeling/model-components/attributes/date-type.html @@ -0,0 +1,41 @@ + + + + + + + + + [DateType] | Coalesce + + + + +

    [DateType]

    Specifies whether a DateTime type will have a date and a time, or only a date.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    [DateType(DateTypeAttribute.DateTypes.DateOnly)]
    +    public DateTimeOffset? BirthDate { get; set; }
    +}
    +

    Properties

    // Also settable via constructor parameter #1
    +public DateTypes DateType { get; set; } = DateTypes.DateTime; 

    The type of date the property represents.

    Enum values are:

    • DateTypeAttribute.DateTypes.DateTime Subject is both a date and time.
    • DateTypeAttribute.DateTypes.DateOnly Subject is only a date with no significant time component.
    + + + diff --git a/modeling/model-components/attributes/default-order-by.html b/modeling/model-components/attributes/default-order-by.html new file mode 100644 index 000000000..ab488c95c --- /dev/null +++ b/modeling/model-components/attributes/default-order-by.html @@ -0,0 +1,59 @@ + + + + + + + + + [DefaultOrderBy] | Coalesce + + + + +

    [DefaultOrderBy]

    Allows setting of the default manner in which the data returned to the client will be sorted. Multiple fields can be used to sort an object by specifying an index.

    This affects the sort order both when requesting a list of the model itself, as well as when the model appears as a child collection off of a navigation property of another object.

    In the first case (a list of the model itself), this can be overridden by setting the orderBy or orderByDescending property on the TypeScript ListViewModel - see TypeScript List ViewModels.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    
    +    public int DepartmentId { get; set; }
    +
    +    [DefaultOrderBy(FieldOrder = 0, FieldName = nameof(Department.Order))]
    +    public Department Department { get; set; }
    +    
    +    [DefaultOrderBy(FieldOrder = 1)]
    +    public string LastName { get; set; }
    +}
    +
    public class Person
    +{
    +    public int PersonId { get; set; }
    +    
    +    public int DepartmentId { get; set; }
    +
    +    [DefaultOrderBy(FieldOrder = 0, FieldName = nameof(Department.Order))]
    +    public Department Department { get; set; }
    +    
    +    [DefaultOrderBy(FieldOrder = 1)]
    +    public string LastName { get; set; }
    +}
    +

    Properties

    // Also settable via constructor parameter #1
    +public int FieldOrder { get; set; } = 0; 

    Specify the index of this field when sorting by multiple fields.

    Lower-valued properties will be used first; higher-valued properties will be used as a tiebreaker (i.e. .ThenBy(...)).

    // Also settable via constructor parameter #2
    +public OrderByDirections OrderByDirection { get; set; } = OrderByDirections.Ascending;

    Specify the direction of the ordering for the property.

    Enum values are:

    • DefaultOrderByAttribute.OrderByDirections.Ascending
    • DefaultOrderByAttribute.OrderByDirections.Descending

    public string FieldName { get; set; }

    When using the DefaultOrderByAttribute on an object property, specifies the field on the object to use for sorting. See the first example above.

    + + + diff --git a/modeling/model-components/attributes/dto-includes-excludes.html b/modeling/model-components/attributes/dto-includes-excludes.html new file mode 100644 index 000000000..a2d188eee --- /dev/null +++ b/modeling/model-components/attributes/dto-includes-excludes.html @@ -0,0 +1,76 @@ + + + + + + + + + [DtoIncludes] & [DtoExcludes] | Coalesce + + + + +

    [DtoIncludes] & [DtoExcludes]

    Allows for easily controlling what data gets set to the client. When requesting data from the generated client-side list view models, you can specify an includes property on the ViewModel or ListViewModel.

    For more information about the includes string, see Includes String.

    When the database entries are returned to the client they will be trimmed based on the requested includes string and the values in DtoExcludes and DtoIncludes.

    DANGER

    These attributes are not security attributes - consumers of your application's API can set the includes string to any value when making a request.

    Do not use them to keep certain data private - use the Security Attributes family of attributes for that.

    It is important to note that the value of the includes string will match against these attributes on any of your models that appears in the object graph being mapped to DTOs - it is not limited only to the model type of the root object.

    Important

    DtoIncludes does not ensure that specific data will be loaded from the database. It only permits what is already loaded into the current EF DbContext to be returned from the API. See Data Sources to learn how to control what data gets loaded from the database.

    Example Usage

    Server code:

    public class Person
    +{
    +    // Don't include CreatedBy when editing - will be included for all other views
    +    [DtoExcludes("Editor")]
    +    public AppUser CreatedBy { get; set; }
    +
    +    // Only include the Person's Department when `includes == "details"` on the TypeScript ViewModel.
    +    [DtoIncludes("details")]
    +    public Department Department { get; set; }
    +
    +    // LastName will be included in all views
    +    public string LastName { get; set; }
    +}
    +
    +public class Department
    +{
    +    [DtoIncludes("details")]
    +    public ICollection<Person> People { get; set; }
    +}
    +

    Client code:

    import { PersonListViewModel } from '@/viewmodels.g'
    +
    +const personList = new PersonListViewModel();
    +personList.$includes = "Editor";
    +await personList.$load();
    +// Objects in personList.$items will not contain CreatedBy nor Department objects.
    +
    +const personList2 = new PersonListViewModel();
    +personList2.$includes = "details";
    +await personList.$load();
    +// Objects in personList2.items will be allowed to contain both CreatedBy and Department objects. 
    +// Department will be allowed to include its other Person objects.
    +

    Properties

    // Also settable via constructor parameter #1
    +public string ContentViews { get; set; }

    A comma-delimited list of values of includes on which to operate.

    For DtoIncludes, this will be the values of includes for which this property will be allowed to be serialized and sent to the client.

    For DtoExcludes, this will be the values of includes for which this property will not be serialized and sent to the client.

    + + + diff --git a/modeling/model-components/attributes/execute.html b/modeling/model-components/attributes/execute.html new file mode 100644 index 000000000..a2e14bb66 --- /dev/null +++ b/modeling/model-components/attributes/execute.html @@ -0,0 +1,44 @@ + + + + + + + + + [Execute] | Coalesce + + + + +

    [Execute]

    Controls permissions for executing of a static or instance method through the API.

    For other security controls, see Security Attributes.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    
    +    [Coalesce, Execute(Roles = "Payroll,HR")]
    +    public void GiveRaise(int centsPerHour) {
    +        ...
    +    }
    +
    +    ...
    +}
    +

    Properties

    public string Roles { get; set; }

    A comma-separated list of roles which are allowed to execute the method.

    public SecurityPermissionLevels PermissionLevel { get; set; } = SecurityPermissionLevels.AllowAuthorized;

    The level of access to allow for the action for the method.

    Enum values are:

    • SecurityPermissionLevels.AllowAll Allow all users to perform the action for the attribute, including users who are not authenticated at all.
    • SecurityPermissionLevels.AllowAuthorized Allow only users who are members of the roles specified on the attribute to perform the action. If no roles are specified on the attribute, then all authenticated users are allowed (no anonymous access).
    • SecurityPermissionLevels.DenyAll Deny the action to all users, regardless of authentication status or authorization level. If DenyAll is used, no API endpoint for the action will be generated.

    public bool AutoClear { get; set; }

    If true, the method's arguments will be cleared after a successful invocation on admin pages.

    public bool? ValidateAttributes { get; set; }

    If non-null, overrides the value of CoalesceOptions.ValidateAttributesForMethods when determining whether to perform automatic server-side validation of the method's parameters.

    If validation is performed, the method's parameters will be validated by the server and the method invocation prevented if errors are found.

    + + + diff --git a/modeling/model-components/attributes/hidden.html b/modeling/model-components/attributes/hidden.html new file mode 100644 index 000000000..1011df01c --- /dev/null +++ b/modeling/model-components/attributes/hidden.html @@ -0,0 +1,41 @@ + + + + + + + + + [Hidden] | Coalesce + + + + +

    [Hidden]

    Mark an property as hidden from the edit, List or All areas.

    DANGER

    This attribute is not a security attribute - it only affects the rendering of the admin pages. It has no impact on data visibility in the API.

    Do not use it to keep certain data private - use the Security Attributes family of attributes for that.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    [Hidden(HiddenAttribute.Areas.All)]
    +    public int? IncomeLevelId { get; set; }
    +}
    +

    Properties

    // Also settable via constructor parameter #1
    +public Areas Area { get; set; } = Areas.All;

    The areas in which the property should be hidden.

    Enum values are:

    • HiddenAttribute.Areas.None Hide from no generated views. Primary and Foreign keys are hidden by default - setting this value explicitly can override this default behavior.
    • HiddenAttribute.Areas.All Hide from all generated views
    • HiddenAttribute.Areas.List Hide from generated list views only (Knockout Table/Cards, Vue c-admin-table)
    • HiddenAttribute.Areas.Edit Hide from generated editor only (Knockout CreateEdit, Vue c-admin-editor)
    + + + diff --git a/modeling/model-components/attributes/inject.html b/modeling/model-components/attributes/inject.html new file mode 100644 index 000000000..e4f2213be --- /dev/null +++ b/modeling/model-components/attributes/inject.html @@ -0,0 +1,45 @@ + + + + + + + + + [Inject] | Coalesce + + + + +

    [Inject]

    ========

    Used to mark a Method parameter for dependency injection from the application's IServiceProvider.

    See Methods for more.

    This gets translated to a Microsoft.AspNetCore.Mvc.FromServicesAttribute in the generated API controller's action.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string FirstName { get; set; }
    +    public string LastName { get; set; }
    +
    +    public string GetFullName([Inject] ILogger<Person> logger)
    +    {
    +        logger.LogInformation("Person " + PersonId + "'s full name was requested");
    +        return FirstName + " " + LastName";
    +    }
    +}
    +
    + + + diff --git a/modeling/model-components/attributes/internal-use.html b/modeling/model-components/attributes/internal-use.html new file mode 100644 index 000000000..b24835089 --- /dev/null +++ b/modeling/model-components/attributes/internal-use.html @@ -0,0 +1,53 @@ + + + + + + + + + [InternalUse] | Coalesce + + + + +

    [InternalUse]

    Used to mark a type, property or method for internal use. Internal Use members are:

    • Not exposed via the API.
    • Not present in the generated TypeScript view models.
    • Not present nor accounted for in the generated C# DTOs.
    • Not present in the generated editor or list views.

    Effectively, an Internal Use member is invisible to Coalesce. This attribute can be considered a Security Attribute.

    Note that this only needs to be used on members that are public. Non-public members (including internal) are always invisible to Coalesce.

    Example Usage

    In this example, Color is the property exposed to the API, but ColorHex is the property that maps to the database that stores the value. A helper method also exists for the color generation, but needs no attribute to be hidden since methods must be explicitly exposed with [Coalesce].

    If no color is saved in the database (the user hasn't picked a color), one is deterministically created.

    public class ApplicationUser
    +{
    +    public int ApplicationUserId { get; set; }
    +
    +    [InternalUse]
    +    public string ColorHex { get; set; }
    +
    +    [NotMapped]
    +    public string Color
    +    {
    +        get => ColorHex ?? GenerateColor(ApplicationUserId).ToRGBHexString();
    +        set => ColorHex = value;
    +    }
    +
    +    public static HSLColor GenerateColor(int? seed = null)
    +    {
    +        var random = seed.HasValue ? new Random(seed.Value) : new Random();
    +        return new HSLColor(random.NextDouble(), random.Next(40, 100) / 100d, random.Next(25, 65) / 100d);
    +    }
    +}
    +
    + + + diff --git a/modeling/model-components/attributes/list-text.html b/modeling/model-components/attributes/list-text.html new file mode 100644 index 000000000..683ed9c35 --- /dev/null +++ b/modeling/model-components/attributes/list-text.html @@ -0,0 +1,45 @@ + + + + + + + + + [ListText] | Coalesce + + + + +

    [ListText]

    When a textual representation of an object needs to be displayed in the UI, this attribute controls which property will be used. Examples include dropdowns and cells in admin UI tables.

    If this attribute is not used, and a property named Name exists on the model, that property will be used. Otherwise, the primary key will be used.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    public string FirstName { get; set; }
    +
    +    public string LastName { get; set; }
    +
    +    [ListText]
    +    [NotMapped]
    +    public string Name => FirstName + " " + LastName
    +}
    +
    + + + diff --git a/modeling/model-components/attributes/load-from-data-source.html b/modeling/model-components/attributes/load-from-data-source.html new file mode 100644 index 000000000..b75d2a353 --- /dev/null +++ b/modeling/model-components/attributes/load-from-data-source.html @@ -0,0 +1,45 @@ + + + + + + + + + [LoadFromDataSource] | Coalesce + + + + +

    [LoadFromDataSource]

    Specifies that the targeted model instance method should load the instance it is called on from the specified data source when invoked from an API endpoint. By default, the default data source for the model's type will be used.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string FirstName { get; set; }
    +
    +    [Coalesce, LoadFromDataSource(typeof(WithoutCases))]
    +    public void ChangeSpacesToDashesInName()
    +    {
    +        FirstName = FirstName.Replace(" ", "-");
    +    }
    +}
    +

    Properties

    // ONLY settable via constructor parameter #1
    +public Type DataSourceType { get; }

    The name of the Data Source to load the instance object from.

    + + + diff --git a/modeling/model-components/attributes/many-to-many.html b/modeling/model-components/attributes/many-to-many.html new file mode 100644 index 000000000..d04b035a6 --- /dev/null +++ b/modeling/model-components/attributes/many-to-many.html @@ -0,0 +1,43 @@ + + + + + + + + + [ManyToMany] | Coalesce + + + + +

    [ManyToMany]

    Used to specify a Many to Many relationship. Because EF core does not support automatic intermediate mapping tables, this field is used to allow for direct reference of the many-to-many collections from the ViewModel.

    The named specified in the attribute will be used as the name of a collection of the objects on the other side of the relationship in the generated TypeScript ViewModels.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +    public string FirstName { get; set; }
    +    public string LastName { get; set; }
    +
    +    [ManyToMany("Appointments")]
    +    public ICollection<PersonAppointment> PersonAppointments { get; set; }
    +}
    +

    Properties

    // ONLY settable via constructor parameter #1
    +public string CollectionName { get; }

    The name of the collection that will contain the set of objects on the other side of the many-to-many relationship.

    public string FarNavigationProperty { get; set; }

    The name of the navigation property on the middle entity that points at the far side of the many-to-many relationship. Use this to resolve ambiguities when the middle table of the many-to-many relationship has more than two reference navigation properties on it.

    + + + diff --git a/modeling/model-components/attributes/search.html b/modeling/model-components/attributes/search.html new file mode 100644 index 000000000..f740c47c3 --- /dev/null +++ b/modeling/model-components/attributes/search.html @@ -0,0 +1,51 @@ + + + + + + + + + [Search] | Coalesce + + + + +

    [Search]

    Coalesce supports searching through the generated API in its various implementations, including the generated list views (Table & Cards), in Select2 dropdowns, and directly through the TypeScript ListViewModels' search property.

    The search parameter of the API can also be formatted as PropertyName:SearchTerm in order to search on an arbitrary property of a model. For example, a value of Nickname:Steve-o for a search term would search the Nickname property, even through it is not marked as searchable using this attribute.

    By default, the system will search any field with the name 'Name'. If this doesn't exist, the ID is used as the only searchable field. Once you place the Search attribute on one or more properties on a model, only those annotated properties will be searched.

    Searchable Property Types

    Strings

    String fields will be searched based on the SearchMethod property on the attribute. See below.

    Numeric Types

    If the input is numeric, numeric fields will be searched for the exact value.

    Enums

    If the input is a valid name of an enum value for an enum property and that property is searchable, rows will be searched for the exact value.

    Dates

    If the input is a parsable date, rows will be searched based on that date.

    Date search will do its best to guess at the user's intentions:

    • Various forms of year/month combos are supported, and if only a year/month is inputted, it will look for all dates in that month, e.g. "Feb 2017" or "2016-11".
    • A date without a time (or a time of exactly midnight) will search the entire day, e.g. "2017/4/18".
    • A date/time with minutes and seconds equal to 0 will search the entire hour, e.g. "April 7, 2017 11 AM".

    TIP

    When searching on date properties, you should almost always set IsSplitOnSpaces = false on the Search attribute. This allows natural inputs like "July 21, 2017" to search correctly. Otherwise, only non-whitespace date formats will work, like "2017/21/07".

    Reference Navigation Properties

    When a reference navigation property is marked with [Search], searchable properties on the referenced object will also be searched. This behavior will go up to two levels away from the root object, and can be controlled with the RootWhitelist and RootBlacklist properties on the [Search] attribute that are outlined below.

    Collection Navigation Properties

    When a collection navigation property is marked with [Search], searchable properties on the child objects will also be searched. This behavior will go up to two levels away from the root object, and can be controlled with the RootWhitelist and RootBlacklist properties on the [Search] attribute that are outlined below.

    WARNING

    Searches on collection navigation properties usually don't translate well with EF Core, leading to potentially degraded performance. Use this feature cautiously.

    Example Usage

    public class Person
    +{
    +    public int PersonId { get; set; }
    +
    +    [Search]
    +    public string FirstName { get; set; }
    +
    +    [Search]
    +    public string LastName { get; set; }
    +
    +    [Search(IsSplitOnSpaces = false)]
    +    public string BirthDate { get; set; }
    +
    +    public string Nickname { get; set; }
    +
    +    [Search(RootWhitelist = nameof(Person))]
    +    public ICollection<Address> Addresses { get; set; }
    +}
    +

    Properties

    public bool IsSplitOnSpaces { get; set; } = true;

    If set to true (the default), each word in the search terms will be searched for in each searchable field independently, and a row will only be considered a match if each word in the search term is a match on at least one searchable property where IsSplitOnSpaces == true

    This is useful when searching for a full name across two or more fields. In the above example, using IsSplitOnSpaces = true would provide more intuitive behavior since it will search both first name and last name for each word entered into the search field. But, you probably shouldn't be doing thatopen in new window.

    public SearchMethods SearchMethod { get; set; } = SearchMethods.BeginsWith;

    For string properties, specifies how the value in the property/column will be matched.

    • BeginsWith: Search term will be checked for at the beginning of the field's value in a case insensitive manner.
    • Equals: Search term must match the field exactly in a case insensitive manner.
    • EqualsNatural: Search term must match exactly, using the natural casing handling of the evaluation environment. Default database collation will be used if evaluated in SQL, and exact casing will be used if evaluated in memory. This allows index seeks to be used instead of index scans, providing extra high performance searches against indexed columns
    • Contains: Search term will be checked for anywhere inside the field's value in a case insensitive manner. Will be slow against large databases - performance cannot be improved with database indexing.

    public string RootWhitelist { get; set; } = null;

    A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched unless the root object of the API call was one of the specified class names.

    public string RootBlacklist { get; set; } = null;

    A comma-delimited list of model class names that, if set, will prevent the targeted property from being searched if the root object of the API call was one of the specified class names.

    + + + diff --git a/modeling/model-components/attributes/security-attribute.html b/modeling/model-components/attributes/security-attribute.html new file mode 100644 index 000000000..74b5004eb --- /dev/null +++ b/modeling/model-components/attributes/security-attribute.html @@ -0,0 +1,65 @@ + + + + + + + + + Security Attributes | Coalesce + + + + +

    Security Attributes

    Coalesce provides a collection of four attributes which can provide class-level (and property-level, where appropriate) security controls over the generated API.

    TIP

    This page provides API-level documentation for a specific set of four attributes. For a complete overview of all the security-focused techniques that can be used in a Coalesce application, see the Security page.

    Class vs. Property Security

    There are important differences between class-level security and property-level security, beyond the usage of the attributes themselves. In general, class-level security is implemented in the generated API Controllers as [Authorize] attributes on the generated actions. Property security attributes are implemented in the Generated C# DTOs.

    Implementations

    [Read]

    Controls permissions for reading of objects and properties through the API.

    For property-level security only, if a [Read] attribute is present without an [Edit] attribute, the property is read-only.

    Additionally, you can set NoAutoInclude = true the [Read] attribute to suppress the Default Loading Behavior.

    Example Usage

    [Read(Roles = "Management", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +
    +    [Read("Payroll")]
    +    public string LastFourSsn { get; set; }
    +    
    +    ...
    +}
    +

    [Edit]

    Controls permissions for editing of objects and properties through the API.

    For property-level security only, if a [Read] attribute is present, one of its roles must be fulfilled in addition to the roles specified (if any) for the [Edit] attribute.

    Example Usage

    [Edit(Roles = "Management,Payroll", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +
    +    [Read("Payroll,HumanResources"), Edit("Payroll")]
    +    public string LastFourSsn { get; set; }
    +    
    +    ...
    +}
    +

    [Create]

    Controls permissions for creation of an object of the targeted type through the API.

    Example Usage

    [Create(Roles = "HumanResources", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    ...
    +}
    +

    [Delete]

    Controls permissions for deletion of an object of the targeted type through the API.

    Example Usage

    [Delete(Roles = "HumanResources,Management", PermissionLevel = SecurityPermissionLevels.AllowAuthorized)]
    +public class Employee
    +{
    +    ...
    +}
    +

    [Execute]

    A separate attribute for controlling method execution exists. Its documentation may be found on the [Execute] page.

    Attribute Properties

    // Also settable via constructor parameter #1
    +public string Roles { get; set; }

    A comma-delimited list of roles that are authorized to take perform the action represented by the attribute. If the current user belongs to any of the listed roles, the action will be allowed.

    The string set for this property will be outputted as an [Authorize(Roles="RolesString")] attribute on generated API controller actions.

    // Also settable via constructor parameter #2
    +public SecurityPermissionLevels PermissionLevel { get; set; }

    The level of access to allow for the action for class-level security only. Has no effect for property-level security.

    Enum values are:

    • SecurityPermissionLevels.AllowAll Allow all users to perform the action for the attribute, including users who are not authenticated at all.
    • SecurityPermissionLevels.AllowAuthorized Allow only users who are members of the roles specified on the attribute to perform the action. If no roles are specified on the attribute, then all authenticated users are allowed (no anonymous access).
    • SecurityPermissionLevels.DenyAll Deny the action to all users, regardless of authentication status or authorization level. If DenyAll is used, no API endpoint for the action will be generated.
    + + + diff --git a/modeling/model-components/attributes/select-filter.html b/modeling/model-components/attributes/select-filter.html new file mode 100644 index 000000000..427539518 --- /dev/null +++ b/modeling/model-components/attributes/select-filter.html @@ -0,0 +1,53 @@ + + + + + + + + + [SelectFilter] | Coalesce + + + + +

    [SelectFilter]

    WARNING

    This attribute only affects the generated Knockout HTML views - it does not enforce any relational rules in your data.

    This attribute also currently has no effect against the Vue stack.

    Specify a property to restrict dropdown menus by. Values presented will be only those where the value of the foreign property matches the value of the local property.

    The local property name defaults to the same value of the foreign property.

    Additionally, in place of a LocalPropertyName to check against, you may instead specify a static value using StaticPropertyValue to filter by a constant.

    Example Usage

    In this example, a dropdown for EmployeeRank created using @Knockout.SelectForObject in cshtml files will only present possible values of EmployeeRank which are valid for the EmployeeType of the Employee.

    public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +    public int EmployeeTypeId { get; set; }
    +    public EmployeeType EmployeeType { get; set; }
    +    public int EmployeeRankId { get; set; }
    +
    +    [SelectFilter(ForeignPropertyName = nameof(EmployeeRank.EmployeeTypeId), LocalPropertyName = nameof(Employee.EmployeeTypeId))]
    +    public EmployeeRank EmployeeRank { get; set; }
    +}
    +
    +public class EmployeeRank
    +{
    +    public int EmployeeRankId { get; set; }
    +    public int EmployeeTypeId { get; set; }
    +    public EmployeeType EmployeeType { get; set; }
    +}
    +
    <div>
    +    @(Knockout.SelectForObject<Models.Employee>(e => e.EmployeeRank))
    +</div>
    +

    Properties

    public string ForeignPropertyName { get; set; }

    The name of the property on the foreign object to filter against.

    public string LocalPropertyName { get; set; }

    The name of another property belonging to the class in which this attribute is used. The results of select lists will be filtered to match this value.

    Defaults to the value of ForeignPropertyName if not set.

    public string LocalPropertyObjectName { get; set; }

    If specified, the LocalPropertyName will be resolved from the property by this name that resides on the local object.

    This allows for querying against properties that are one level away from the current object.

    public string StaticPropertyValue { get; set; }

    A constant value that the foreign property will be filtered against. This string must be parsable into the foreign property's type to have any effect. If this is set, LocalPropertyName will be ignored.

    + + + diff --git a/modeling/model-components/attributes/typescript-partial.html b/modeling/model-components/attributes/typescript-partial.html new file mode 100644 index 000000000..3534ab68d --- /dev/null +++ b/modeling/model-components/attributes/typescript-partial.html @@ -0,0 +1,40 @@ + + + + + + + + + [TypeScriptPartial] | Coalesce + + + + +

    [TypeScriptPartial]

    Note

    This attribute only applies to the Knockout front-end stack. It is not applicable to the Vue stack.

    If defined on a model, a typescript file will be generated in ./Scripts/Partials if one does not already exist. This 'Partial' TypeScript file contains a class which inherits from the generated TypeScript ViewModel. The partial class has the same name as the generated ViewModel would normally have, and the generated ViewModel is renamed to "<ClassName>Partial".

    This behavior allows you to extend the behavior of the generated TypeScript view models with your own properties and methods for defining more advanced behavior on the client. One of the most common use cases of this is to define additional Knockout ComputedObservable properties for information that is only useful in the browser - for example, computing a css class based on data in the object.

    Example Usage

    [TypeScriptPartial]
    +public class Employee
    +{
    +    public int EmployeeId { get; set; }
    +
    +    ...
    +}
    +

    Properties

    public string BaseClassName { get; set; }

    If set, overrides the name of the generated ViewModel which becomes the base class for the generated 'Partial' TypeScript file.

    + + + diff --git a/modeling/model-components/behaviors.html b/modeling/model-components/behaviors.html new file mode 100644 index 000000000..d9d6db111 --- /dev/null +++ b/modeling/model-components/behaviors.html @@ -0,0 +1,106 @@ + + + + + + + + + Behaviors | Coalesce + + + + +

    Behaviors

    In a CRUD system, creating, updating, and deleting are considered especially different from reading. In Coalesce, the dedicated classes that perform these operations are derivatives of a special interface known as the IBehaviors<T>. These are their stories.


    Coalesce separates out the parts of your API that read your data from the parts that mutate it. The read portion is performed by Data Sources, and the mutations are performed by behaviors. Like data sources, there exists a standard set of behaviors that Coalesce provides out-of-the-box that cover the most common use cases for creating, updating, and deleting objects in your data model.

    Also like data sources, these functions can be easily overridden on a per-model basis, allowing complete control over the ways in which your data is mutated by the APIs that Coalesce generates. However, unlike data sources which can have as many implementations per model as you like, you can only have one set of behaviors.

    Defining Behaviors

    By default, each of your models that Coalesce exposes will utilize the standard behaviors (IntelliTect.Coalesce.StandardBehaviors<T, TContext>) for the out-of-the-box API endpoints that Coalesce provides. These behaviors provide a set of create, update, and delete methods for an EF Core DbContext, as well as a plethora of virtual methods that make the StandardBehaviors a great base class for your custom implementations. Unlike data sources which require an annotation to override the Coalesce-provided standard class, the simple presence of an explicitly declared set of behaviors will suppress the standard behaviors.

    Note

    When you define a set of custom behaviors, take note that these are only used by the standard set of API endpoints that Coalesce always provides. They will not be used to handle any mutations in any Methods you write for your models.

    To create your own behaviors, you simply need to define a class that implements IntelliTect.Coalesce.IBehaviors<T>. To expose your behaviors to Coalesce, either place it as a nested class of the type T that your behaviors are for, or annotate it with the [Coalesce] attribute. Of course, the easiest way to create behaviors that doesn't require you to re-engineer a great deal of logic would be to inherit from IntelliTect.Coalesce.StandardBehaviors<T, TContext>, and then override only the parts that you need.

    public class Case
    +{
    +    public int CaseId { get; set; }
    +    public int OwnerId { get; set; }
    +    public bool IsDeleted { get; set; }
    +    ...
    +}
    +
    +[Coalesce]
    +public class CaseBehaviors : StandardBehaviors<Case, AppDbContext>
    +{
    +    public CaseBehaviors(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override ItemResult BeforeSave(SaveKind kind, Case oldItem, Case item)
    +    {
    +        // Allow admins to bypass all validation.
    +        if (User.IsInRole("Admin")) return true;
    +
    +        if (kind == SaveKind.Update && oldItem.OwnerId != item.OwnerId)
    +            return "The owner of a case may not be changed";
    +
    +        // This is a new item, OR its an existing item and the owner isn't being modified.
    +        if (item.CreatedById != User.GetUserId())
    +            return "You are not the owner of this item.";
    +
    +        return true;
    +    }
    +
    +    public override ItemResult BeforeDelete(Case item) 
    +        => User.IsInRole("Manager") ? true : "Unauthorized";
    +
    +    public override Task ExecuteDeleteAsync(Case item)
    +    {
    +        // Soft delete the item.
    +        item.IsDeleted = true;
    +        return Db.SaveChangesAsync();
    +    }
    +}
    +

    Dependency Injection

    All behaviors are instantiated using dependency injection and your application's IServiceProvider. As a result, you can add whatever constructor parameters you desire to your behaviors as long as a value for them can be resolved from your application's services. The single parameter to the StandardBehaviors is resolved in this way - the CrudContext<TContext> contains the common set of objects most commonly used, including the DbContext and the ClaimsPrincipal representing the current user.

    Standard Behaviors

    The standard behaviors, IntelliTect.Coalesce.StandardBehaviors<T> and its EntityFramework-supporting sibling IntelliTect.Coalesce.StandardBehaviors<T, TContext>, contain a significant number of properties and methods that can be utilized and/or overridden at your leisure.

    Properties

    CrudContext<TContext> Context

    The object passed to the constructor that contains the set of objects needed by the standard behaviors, and those that are most likely to be used in custom implementations.

    TContext Db

    An instance of the db context that contains a DbSet<T> for the entity handled by the behaviors

    ClaimsPrincipal User

    The user making the current request.

    IDataSource<T> OverrideFetchForUpdateDataSource

    A data source that, if set, will override the data source that is used to retrieve the target of an update operation from the database. The incoming values will then be set on this retrieved object. Null by default; override by setting a value in the constructor.

    IDataSource<T> OverridePostSaveResultDataSource

    A data source that, if set, will override the data source that is used to retrieve a newly-created or just-updated object from the database after a save. The retrieved object will be returned to the client. Null by default; override by setting a value in the constructor.

    IDataSource<T> OverrideFetchForDeleteDataSource

    A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database. The retrieved object will then be deleted. Null by default; override by setting a value in the constructor.

    IDataSource<T> OverridePostDeleteResultDataSource

    A data source that, if set, will override the data source that is used to retrieve the target of an delete operation from the database after it has been deleted. If an object is able to be retrieved from this data source, it will be sent back to the client. This allows soft-deleted items to be returned to the client when the user is able to see them. Null by default; override by setting a value in the constructor.

    Method Overview

    The standard behaviors implementation contains many different methods which can be overridden in your derived class to control functionality.

    These methods often call one another, so overriding one method may cause some other method to no longer be called. The hierarchy of method calls, ignoring any logic or conditions contained within, is as follows:

    SaveAsync
    +    DetermineSaveKindAsync
    +    GetDbSet
    +    ValidateDto
    +    MapIncomingDto
    +    BeforeSaveAsync
    +        BeforeSave
    +    ExecuteSaveAsync
    +    AfterSave
    +
    +DeleteAsync
    +    BeforeDeleteAsync
    +        BeforeDelete
    +    ExecuteDeleteAsync
    +        GetDbSet
    +    AfterDelete
    +

    Method Details

    All of the methods outlined above can be overridden. A description of each of the methods is as follows:

    Task<ItemResult<TDto?>> SaveAsync<TDto>(TDto incomingDto, IDataSource<T> dataSource, IDataSourceParameters parameters)

    Save the given item. This is the main entry point for saving, and takes a DTO as a parameter. This method is responsible for performing mapping to your EF models and ultimately saving to your database. If it is required that you access properties from the incoming DTO in this method, a set of extension methods GetValue and GetObject are available on the DTO for accessing properties that are mapped 1:1 with your EF models.

    Task<(SaveKind Kind, object? IncomingKey)> DetermineSaveKindAsync<TDto>(TDto incomingDto, IDataSource<T> dataSource, IDataSourceParameters parameters)

    Given the incoming DTO on which Save has been called, examine its properties to determine if the operation is meant to be a create or an update operation. Return this distinction along with the key that was used to make the distinction.

    This method is called outside of the standard data source by the base API controller to perform role-based security on saves at the controller level.

    DbSet<T> GetDbSet()

    Returns a DbSet<T> that items can be added to (creates) or remove from (deletes).

    ItemResult ValidateDto(SaveKind kind, IClassDto<T> dto)

    Provides a chance to validate the properties of the DTO object itself, as opposed to doing validation in BeforeSave of the properties of the model after the DTO has been mapped to the model. This also where attribute-based validation is performed.

    To perform custom validation in this method (uncommon), there are a number of extension methods on IClassDto<T> that can be used to access the value of the properties of Generated C# DTOs. For behaviors on Custom DTOs where the DTO type is known, simply cast to the correct type.

    T MapIncomingDto<TDto>(SaveKind kind, T? item, TDto dto, IDataSourceParameters parameters)

    Map the properties of the incoming DTO to the model that will be saved to the database. For a SaveKind.Create, this will call the MapToNew method on the DTO and a new instance must be returned (item will be null). For a SaveKind.Update, this will call the MapTo method on the DTO, and the incoming item must be returned. If more precise control is needed, extension methods on IClassDto<T> or casting to a known type can be used to get specific values. If all else fails, the DTO can be reflected upon.

    Task<ItemResult> BeforeSaveAsync(SaveKind kind, T? oldItem, T item);
    +ItemResult BeforeSave(SaveKind kind, T? oldItem, T item)

    Provides an easy way for derived classes to intercept a save attempt and either reject it by returning an unsuccessful result, or approve it by returning success. The incoming item can also be modified at will in this method to override changes that the client made as desired.

    ItemResult AfterSave(SaveKind kind, T? oldItem, ref T item, ref IncludeTree? includeTree)

    Provides an easy way for derived classes to perform actions after a save operation has been completed. Failure results returned here will present an error to the client, but will not prevent modifications to the database since changes have already been saved at this point. This method can optionally modify or replace the item that is sent back to the client after a save by setting ref T item to another object or to null. Setting ref IncludeTree includeTree will override the Include Tree used to shape the response object.

    WARNING

    Setting ref T item to null will prevent the new object from being returned - be aware that this can be harmful in create scenarios since it prevents the client from receiving the primary key of the newly created item. If autoSave is enabled on the client, this could cause a large number of duplicate objects to be created in the database, since each subsequent save by the client will be treated as a create when the incoming object lacks a primary key.

    Task<ItemResult<TDto?>> DeleteAsync<TDto>(object id, IDataSource<T> dataSource, IDataSourceParameters parameters)

    Deletes the given item.

    Task<ItemResult> BeforeDeleteAsync(T item);
    +ItemResult BeforeDelete(T item)

    Provides an easy way to intercept a delete request and potentially reject it (by returning a non-success ItemResult).

    Task ExecuteDeleteAsync(T item)

    Performs the delete action against the database. The implementation of this method removes the item from its corresponding DbSet<T>, and then calls Db.SaveChangesAsync().

    Overriding this allows for changing this row-deletion implementation to something else, like setting of a soft delete flag, or copying the data into another archival table before deleting.

    void AfterDelete(ref T item, ref IncludeTree? includeTree)

    Allows for performing any sort of cleanup actions after a delete has completed. If the item was still able to be retrieved from the database after the delete operation completed, this method allows lets you modify or replace the item that is sent back to the client by setting ref T item to another object or to null. Setting ref IncludeTree includeTree will override the Include Tree used to shape the response object.

    Globally Replacing the Standard Behaviors

    You can, of course, create a custom base behaviors class that all your custom implementations inherit from. But, what if you want to override the standard behaviors across your entire application, so that StandardBehaviors<,> will never be instantiated? You can do that too!

    Simply create a class that implements IEntityFrameworkBehaviors<,> (the StandardBehaviors<,> already does - feel free to inherit from it), then register it at application startup like so:

    public class MyBehaviors<T, TContext> : StandardBehaviors<T, TContext>
    +    where T : class
    +    where TContext : DbContext
    +{
    +    public MyBehaviors(CrudContext<TContext> context) : base(context)
    +    {
    +    }
    +
    +    ...
    +}
    +
    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddCoalesce(b =>
    +    {
    +        b.AddContext<AppDbContext>();
    +        b.UseDefaultBehaviors(typeof(MyBehaviors<,>));
    +    });
    +

    Your custom behaviors class must have the same generic type parameters - <T, TContext>. Otherwise, the Microsoft.Extensions.DependencyInjection service provider won't know how to inject it.

    + + + diff --git a/modeling/model-components/data-sources.html b/modeling/model-components/data-sources.html new file mode 100644 index 000000000..7c186f596 --- /dev/null +++ b/modeling/model-components/data-sources.html @@ -0,0 +1,152 @@ + + + + + + + + + Data Sources | Coalesce + + + + +

    Data Sources

    In Coalesce, all data that is retrieved from your database through the generated controllers is done so by a data source. These data sources control what data gets loaded and how it gets loaded. By default, there is a single generic data source that serves all data for your models in a generic way that fits many of the most common use cases - the Standard Data Source.

    In addition to this standard data source, Coalesce allows you to create custom data sources that provide complete control over the way data is loaded and serialized for transfer to a requesting client. These data sources are defined on a per-model basis, and you can have as many of them as you like for each model.

    Defining Data Sources

    By default, each of your models that Coalesce exposes will expose the standard data source (IntelliTect.Coalesce.StandardDataSource<T, TContext>). This data source provides all the standard functionality one would expect - paging, sorting, searching, filtering, and so on. Each of these component pieces is implemented in one or more virtual methods, making the StandardDataSource a great place to start from when implementing your own data source. To suppress this behavior of always exposing the raw StandardDataSource, create your own custom data source and annotate it with [DefaultDataSource].

    To implement your own custom data source, you simply need to define a class that implements IntelliTect.Coalesce.IDataSource<T>. To expose your data source to Coalesce, either place it as a nested class of the type T that you data source serves, or annotate it with the [Coalesce] attribute. Of course, the easiest way to create a data source that doesn't require you to re-engineer a great deal of logic would be to inherit from IntelliTect.Coalesce.StandardDataSource<T, TContext>, and then override only the parts that you need.

    public class Person
    +{
    +    [DefaultDataSource]
    +    public class IncludeFamily : StandardDataSource<Person, AppDbContext>
    +    {
    +        public IncludeFamily(CrudContext<AppDbContext> context) : base(context) { }
    +
    +        public override IQueryable<Person> GetQuery(IDataSourceParameters parameters) 
    +            => Db.People
    +            .Where(f => User.IsInRole("Admin") || f.CreatedById == User.GetUserId())
    +            .Include(f => f.Parents).ThenInclude(s => s.Parents)
    +            .Include(f => f.Cousins).ThenInclude(s => s.Parents);
    +    }
    +}
    +
    +[Coalesce]
    +public class NamesStartingWithA : StandardDataSource<Person, AppDbContext>
    +{
    +    public NamesStartingWithA(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Person> GetQuery(IDataSourceParameters parameters) 
    +        => Db.People.Include(f => f.Siblings).Where(f => f.FirstName.StartsWith("A"));
    +}
    +

    The structure of the IQueryable built by the various methods of StandardDataSource is used to shape and trim the structure of the DTO as it is serialized and sent out to the client. One may also override method IncludeTree GetIncludeTree(IQueryable<Person> query, IDataSourceParameters parameters) to control this explicitly. See Include Tree for more information on how this works.

    WARNING

    If you create a custom data source that has custom logic for securing your data, be aware that the default implementation of StandardDataSource (or your custom default implementation - see below) is still exposed unless you annotate one of your custom data sources with [DefaultDataSource]. Doing so will replace the default data source with the annotated class for your type T.

    Dependency Injection

    All data sources are instantiated using dependency injection and your application's IServiceProvider. As a result, you can add whatever constructor parameters you desire to your data sources as long as a value for them can be resolved from your application's services. The single parameter to the StandardDataSource is resolved in this way - the CrudContext<TContext> contains the common set of objects most commonly used, including the DbContext and the ClaimsPrincipal representing the current user.

    Consuming Data Sources

    The ViewModels and ListViewModels each have a property called $dataSource. This property accepts an instance of a DataSource class generated in the Model Layer.

    import { Person } from '@/models.g'
    +import { PersonViewModel, PersonListViewModel } from '@/viewmodels.g'
    +
    +var viewModel = new PersonViewModel();
    +viewModel.$dataSource = new Person.DataSources.IncludeFamily();
    +viewModel.$load(1);
    +
    +var list = new PersonListViewModel();
    +list.$dataSource = new Person.DataSources.NamesStartingWith();
    +list.$load(1);
    +

    Standard Parameters

    All methods on IDataSource<T> take a parameter that contains all the client-specified parameters for things paging, searching, sorting, and filtering information. Almost all virtual methods on StandardDataSource are also passed the relevant set of parameters.

    Custom Parameters

    On any data source that you create, you may add additional properties annotated with [Coalesce] that will then be exposed as parameters to the client. These property parameters are currently restricted to primitives (numeric types, strings) and dates (DateTime, DateTimeOffset). Property parameter primitives may be expanded to allow for more types in the future.

    [Coalesce]
    +public class NamesStartingWith : StandardDataSource<Person, AppDbContext>
    +{
    +    public NamesStartingWith(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    [Coalesce]
    +    public string StartsWith { get; set; }
    +
    +    public override IQueryable<Person> GetQuery(IDataSourceParameters parameters) 
    +        => Db.People.Include(f => f.Siblings)
    +        .Where(f => string.IsNullOrWhitespace(StartsWith) ? true : f.FirstName.StartsWith(StartsWith));
    +}
    +

    List Auto-loading

    You can setup TypeScript List ViewModels to automatically reload from the server when data source parameters change:

    To automatically reload a ListViewModel when data source parameters change, simply use the list's $useAutoLoad or $startAutoLoad function:

    import { Person } from '@/models.g';
    +import { PersonListViewModel } from '@/viewmodels.g';
    +
    +const list = new PersonListViewModel;
    +const dataSource = list.$dataSource = new Person.DataSources.NamesStartingWith
    +list.$useAutoLoad(); // When using options API, use $startAutoLoad(this)
    +
    +// Trigger a reload:
    +dataSource.startsWith = "Jo";
    +

    Standard Data Source

    The standard data sources, IntelliTect.Coalesce.StandardDataSource<T> and its EntityFramework-supporting sibling IntelliTect.Coalesce.StandardDataSource<T, TContext>, contain a significant number of properties and methods that can be utilized and/or overridden at your leisure.

    Default Loading Behavior

    When an object or list of objects is requested, the default behavior of the the StandardDataSource is to load all of the immediate relationships of the object (parent objects and child collections), as well as the far side of many-to-many relationships. This is performed in StandardDataSource.GetQuery(), so in order to suppress this behavior in a custom data source, don't build you query off of base.GetQuery(), but instead start directly from the DbSet for your entity when building your custom query.

    Clients can suppress this per-request by setting includes = "none" on your TypeScript ViewModel or ListViewModel, but note this is not a security mechanism and should only be used to reduce payload size or improve response time.

    On the server, you can suppress this behavior by placing [Read(NoAutoInclude = true)] on either an entire class (affecting all navigation properties of that type), or on specific navigation properties. When placed on a entity class that holds sensitive data, this can help ensure you don't accidentally leak records due to forgetting to customize the data sources of the types whose navigation properties reference your sensitive entity.

    Properties

    The following properties are available for use on the StandardDataSource any any derived instances.

    CrudContext<TContext> Context

    The object passed to the constructor that contains the set of objects needed by the standard data source, and those that are most likely to be used in custom implementations.

    TContext Db

    An instance of the DbContext that contains a DbSet<T> for the entity served by the data source.

    ClaimsPrincipal User

    The user making the current request.

    int MaxSearchTerms

    The max number of search terms to process when interpreting a search term word-by-word. Override by setting a value in the constructor.

    int DefaultPageSize

    The page size to use if none is specified by the client. Override by setting a value in the constructor.

    int MaxPageSize

    The maximum page size that will be served. By default, client-specified page sizes will be clamped to this value. Override by setting a value in the constructor.

    Method Overview

    The standard data source contains 19 different methods which can be overridden in your derived class to control its behavior.

    These methods often call one another, so overriding one method may cause some other method to no longer be called. The hierarchy of method calls, ignoring any logic or conditions contained within, is as follows:

    GetMappedItemAsync
    +    GetItemAsync
    +        GetQueryAsync
    +            GetQuery
    +        GetIncludeTree
    +    TransformResults
    +
    +GetMappedListAsync
    +    GetListAsync
    +        GetQueryAsync
    +            GetQuery
    +        ApplyListFiltering
    +            ApplyListPropertyFilters
    +                ApplyListPropertyFilter
    +            ApplyListSearchTerm
    +        GetListTotalCountAsync
    +        ApplyListSorting
    +            ApplyListClientSpecifiedSorting
    +            ApplyListDefaultSorting
    +        ApplyListPaging
    +        GetIncludeTree
    +    TrimListFields
    +    TransformResults
    +
    +GetCountAsync
    +    GetQueryAsync
    +        GetQuery
    +    ApplyListFiltering
    +        ApplyListPropertyFilters
    +            ApplyListPropertyFilter
    +        ApplyListSearchTerm
    +    GetListTotalCountAsync
    +

    Method Details

    All of the methods outlined above can be overridden. A description of each of the non-interface inner methods is as follows:

    IQueryable<T> GetQuery(IDataSourceParameters parameters);
    +Task<IQueryable<T>> GetQueryAsync(IDataSourceParameters parameters);

    The method is the one that you will most commonly be override in order to implement custom query logic. The default implementation of GetQueryAsync simply calls GetQuery - be aware of this in cases of complex overrides/inheritance. From this method, you could:

    • Specify additional query filtering such as row-level security or soft-delete logic. Or, restrict the data source entirely for users or whole roles by returning an empty query.
    • Include additional data using EF's .Include() and .ThenInclude().
    • Add additional edges to the serialized object graph using Coalesce's .IncludedSeparately() and .ThenIncluded().

    Note

    When GetQuery is overridden, the Default Loading Behavior is overridden as well. To restore this behavior, use the IQueryable<T>.IncludeChildren() extension method to build your query.

    IncludeTree? GetIncludeTree(IQueryable<T> query, IDataSourceParameters parameters)

    Allows for explicitly specifying the Include Tree that will be used when serializing results obtained from this data source into DTOs. By default, the query that is build up through all the other methods in the data source will be used to build the include tree.

    bool CanEvalQueryAsynchronously(IQueryable<T> query)

    Called by other methods in the standard data source to determine whether or not EF Core async methods will be used to evaluate queries. This may be globally disabled when bugs like https://github.com/dotnet/SqlClient/issues/593 are present in EF Core.

    IQueryable<T> ApplyListFiltering(IQueryable<T> query, IFilterParameters parameters)

    A simple wrapper that calls ApplyListPropertyFilters and ApplyListSearchTerm.

    IQueryable<T> ApplyListPropertyFilters(IQueryable<T> query, IFilterParameters parameters)

    For each value in parameters.Filter, invoke ApplyListPropertyFilter to apply a filter to the query.

    IQueryable<T> ApplyListPropertyFilter(IQueryable<T> query, PropertyViewModel prop, string value)

    Given a property and a client-provided string value, perform some filtering on that property.

    • Dates with a time component will be matched exactly.
    • Dates with no time component will match any dates that fell on that day.
    • Strings will match exactly unless an asterisk is found, in which case they will be matched with string.StartsWith.
    • Enums will match by string or numeric value. Multiple comma-delimited values will create a filter that will match on any of the provided values.
    • Numeric values will match exactly. Multiple comma-delimited values will create a filter that will match on any of the provided values.

    IQueryable<T> ApplyListSearchTerm(IQueryable<T> query, IFilterParameters parameters)

    Applies filters to the query based on the specified search term. See [Search] for a detailed look at how searching works in Coalesce.

    IQueryable<T> ApplyListSorting(IQueryable<T> query, IListParameters parameters)

    If any client-specified sort orders are present, invokes ApplyListClientSpecifiedSorting. Otherwise, invokes ApplyListDefaultSorting.

    IQueryable<T> ApplyListClientSpecifiedSorting(IQueryable<T> query, IListParameters parameters)

    Applies sorting to the query based on sort orders specified by the client. If the client specified "none" as the sort field, no sorting will take place.

    IQueryable<T> ApplyListDefaultSorting(IQueryable<T> query)

    Applies default sorting behavior to the query, including behavior defined with use of [DefaultOrderBy] in C# POCOs, as well as fallback sorting to "Name" or primary key properties.

    IQueryable<T> ApplyListPaging(IQueryable<T> query, IListParameters parameters, int? totalCount, out int page, out int pageSize)

    Applies paging to the query based on incoming parameters. Provides the actual page and pageSize that were used as out parameters.

    Task<int> GetListTotalCountAsync(IQueryable<T> query, IFilterParameters parameters)

    Simple wrapper around invoking .Count() on a query.

    void TransformResults(IReadOnlyList<T> results, IDataSourceParameters parameters);
    +Task TransformResultsAsync(IReadOnlyList<T> results, IDataSourceParameters parameters);

    Allows for transformation of a result set after the query has been evaluated. This will be called for both lists of items and for single items. This can be used for populating non-mapped properties on a model, or conditionally loading navigation properties using logic that depends upon the contents of each loaded record.

    This method is only called immediately before mapping to a DTO; it does not affect operations that don't involve mapping to a DTO - e.g. when loading the target of a /save operation or when loading the invocation target of an instance method.

    See the Security page for an example on how to use TransformResults to apply filtered includes.

    Do not use TransformResults to modify any database-mapped scalar properties, since such changes could be inadvertently persisted to the database.

    IList<TDto> TrimListFields<TDto>(IList<TDto> mappedResult, IListParameters parameters)

    Performs trimming of the fields of the result set based on the parameters given to the data source. Can be overridden to forcibly disable this, override the behavior to always trim specific fields, or any other functionality desired.

    Globally Replacing the Standard Data Source

    You can, of course, create a custom base data source that all your custom implementations inherit from. But, what if you want to override the standard data source across your entire application, so that StandardDataSource<,> will never be instantiated? You can do that too!

    Simply create a class that implements IEntityFrameworkDataSource<,> (the StandardDataSource<,> already does - feel free to inherit from it), then register it at application startup like so:

    public class MyDataSource<T, TContext> : StandardDataSource<T, TContext>
    +    where T : class
    +    where TContext : DbContext
    +{
    +    public MyDataSource(CrudContext<TContext> context) : base(context)
    +    {
    +    }
    +
    +    ...
    +}
    +
    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddCoalesce(b =>
    +    {
    +        b.AddContext<AppDbContext>();
    +        b.UseDefaultDataSource(typeof(MyDataSource<,>));
    +    });
    +}
    +

    Your custom data source must have the same generic type parameters - <T, TContext>. Otherwise, the Microsoft.Extensions.DependencyInjection service provider won't know how to inject it.

    + + + diff --git a/modeling/model-components/methods.html b/modeling/model-components/methods.html new file mode 100644 index 000000000..cca834dd7 --- /dev/null +++ b/modeling/model-components/methods.html @@ -0,0 +1,180 @@ + + + + + + + + + Methods | Coalesce + + + + +

    Methods

    Any public methods annotated with the [Coalesce] attribute that are placed on your model classes will have API endpoints and Typescript generated by Coalesce. Both instance methods and static methods are supported. Additionally, any instance methods on Services will also have API endpoints and TypeScript generated.

    These custom methods allow you to implement any custom server-side functionality in your Coalesce application that falls outside of the standard CRUD functions that are generated for your entities.

    Declaring Methods

    Instance Methods

    Instance Methods can be declared on your Entity classes. For example:

    public class User
    +{
    +    public int UserId { get; set; }
    +
    +    public string Email { get; set; }
    +
    +    [Coalesce]
    +    public async Task<ItemResult> SendMessage(
    +        [Inject] SmtpClient client,
    +        ClaimsPrincipal sender,
    +        string message
    +    ) {
    +        if (string.IsNullOrWhitespace(Email)) return "Recipient has no email";
    +        if (string.IsNullOrWhitespace(message)) return "Message is required";
    +
    +        await client.SendMailAsync(new MailMessage(  
    +            from: sender.GetEmailAddress(),
    +            to: Email,
    +            subject: "Message from MyApp",  
    +            body: message
    +        ));
    +        return true;
    +    }
    +}
    +
    +

    When an instance method is invoked, the target model instance will be loaded using the data source specified by an attribute [LoadFromDataSource(typeof(MyDataSource))] if present. Otherwise, the model instance will be loaded using the default data source for the model's type. If you have a Custom Data Source annotated with [DefaultDataSource], that data source will be used. Otherwise, the Standard Data Source will be used. The consequence of this is that a user cannot call a method on an instance of entity that they're not allowed to see or load.

    Instance methods are generated onto the TypeScript ViewModels.

    When should I use Instance Methods?

    Instance methods, as opposed to static or service methods, are a good fit when implementing an action that directly acts on or depends upon a specific instance of one of your entity types. One of their biggest benefits is the automatic row-level security from data sources as described above.

    Static Methods

    Static Methods can be declared on your Entity classes. For example:

    public class Person 
    +{
    +    public int PersonId { get; set; }
    +
    +    public string FirstName { get; set; }
    +
    +    [Coalesce]
    +    public static ICollection<string> NamesStartingWith(
    +        AppDbContext db,
    +        string characters 
    +    ) {
    +        return db.People
    +            .Select(p => p.FirstName)
    +            .Where(f => f.StartsWith(characters))
    +            .ToList();
    +    }
    +}
    +

    Static methods are generated onto the TypeScript ListViewModels. All of the same members that are generated for instance methods are also generated for static methods.

    When should I use Static Methods?

    Static methods are a good fit for actions that don't operate on a specific instance of an entity type, but whose functionality is still closely coupled with a specific, concrete entity type.

    For example, imagine you have a File entity class. You could make a static method on that class that accepts a file as a parameter. This method would persist that file to storage and then save a new entity to the database. You would then disable Create on that entity, since the default /save endpoint cannot accept file uploads.

    Or, imagine an Invoice class. You might make a static method that returns a summary of sales information for a given time range. Since this summarization would be performing aggregate functions against your Invoice entities and is therefore tightly coupled to Invoices, a static method would be suitable.

    Service Methods

    Service methods can be declared on a Coalesce Service class:

    [Coalesce, Service]
    +public class MyService 
    +{
    +  [Coalesce]
    +  public string MyServiceMethod() => "Hello, World!";
    +}
    +

    Or, they can be declared via a Coalesce Service interface that has an implementation registered with dependency injection:

    [Coalesce, Service]
    +public interface IMyService 
    +{
    +  string MyServiceMethod() => "Hello, World!";
    +}
    +

    When declaring service methods by interface, a [Coalesce] attribute on each method is not needed - the entire interface is exposed by Coalesce.

    When should I use Service Methods?

    Services are a catch-all feature and can be used for almost any conceivable purpose in Coalesce to implement custom functionality that needs to be invoked by your front-end app.

    However, there are some reasons why you might not want to use a service:

    • If the method logically operates on a single entity instance, and/or if using an instance method would let you utilize the row-level security already implemented by one of your data sources to authorize who can invoke the method.
    • If the service would only have one or two methods and would logically make sense as a static or instance method. In other words, if adding a new service class would be detrimental to the organization of your codebase and create "file sprawl".

    On the other hand, services have some benefits that instance and static methods cannot provide:

    • Coalesce Services can be declared with an interface, rather than a concrete type, allowing for their implementation to be substituted more easily. For example, a service providing an external integration that you want to mock or stub during automated testing and/or local development.

    Parameters

    The following parameters can be added to your methods:

    TypeDescription

    Primitives, Dates, and other Scalars

    Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, are accepted as parameters to be passed from the client to the method call.

    Entity Models

    When invoking the method on the client, the object's properties will only be serialized one level deep. If an entity model parameter has additional child object properties, they will not be included in the invocation of the method - only the object's primitive & date properties will be deserialized from the client.

    External Types

    Unlike entity model parameters, external type parameters will be serialized and sent by the client to an arbitrarily deep level, excluding any entity model properties that may be nested inside an external type.

    Files

    Methods can accept file uploads by using a parameter of type IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File).

    ICollection<T>open in new window, IEnumerable<T>open in new window

    Collections of any of the above valid parameter types above are also valid parameter types.

    DbContextopen in new window

    If the method has a parameter assignable to Microsoft.EntityFrameworkCore.DbContext, then the parameter will be implicitly [Inject]ed.

    ClaimsPrincipalopen in new window

    If the method has a parameter of type ClaimsPrincipal, the value of HttpContext.User will be passed to the parameter.

    [Inject]

    If a parameter is marked with the [Inject] attribute, it will be injected from the application's IServiceProvider.

    out IncludeTree

    Deprecated. If you need to return an Include Tree to shape the serialization of the method's return value, you should use an ItemResult<T> return value and populate the IncludeTree property on the ItemResult object.

    Return Values

    You can return virtually anything from these methods:

    TypeDescription

    Primitives, Dates, and other Scalars

    Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, may be returned from methods.

    Entity Models

    Any of the types of your models may be returned.

    External Types

    Any External Types you define may also be returned from a method.

    When returning custom types from methods, be careful of the types of their properties. Coalesce will recursively discover and generate code for all public properties of your External Types. If you accidentally include a type that you do not own, these generated types could get out of hand extremely quickly.

    Mark any properties you don't want generated with the [InternalUse] attribute, or give them a non-public access modifier. Whenever possible, don't return types that you don't own or control.

    ICollection<T>open in new window, IEnumerable<T>open in new window

    Collections of any of the above valid return types above are also valid return types. IEnumerables are useful for generator functions using yield. ICollection is highly suggested over IEnumerable whenever appropriate, though.

    IQueryable<T>open in new window

    Queryables of the valid return types above are valid return types. The query will be evaluated, and Coalesce will attempt to pull an Include Tree from the queryable to shape the response.

    When Include Tree functionality is needed to shape the response but an IQueryable<> return type is not feasible, an ItemResult return value with an IncludeTree set on it will do the trick as well.

    Files

    Methods can return file downloads using type IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File).

    Please see the File Downloads section below for more details

    ItemResult<T>, ItemResult, ListResult<T>

    An IntelliTect.Coalesce.Models.ItemResult<T> of any of the valid return types above, including collections, is valid, as well as its non-generic variant ItemResult, and its list variant ListResult<T>.

    Use an ItemResult whenever you might need to signal failure and return an error message from a custom method. The WasSuccessful and Message properties on the result object will be sent along to the client to indicate success or failure of the method. The type T will be mapped to the appropriate DTO object before being serialized as normal.

    An Include Tree can be set on the object's IncludeTree parameter to shape the serialization of the method's returned value.

    Security

    You can implement role-based security on a method by placing the [Execute] on the method. Placing this attribute on the method with no roles specified will simply require that the calling user be authenticated.

    Security for instance methods is also controlled by the data source that loads the instance - if the data source can't provide an instance of the requested model, the method won't be executed.

    See the Security page to read more about custom method security, as well as all other security mechanisms in Coalesce.

    Generated TypeScript

    See API Callers and ViewModel Layer for details on the code that is generated for your custom methods.

    Note

    Any Task-returning methods with "Async" as a suffix to the C# method's name will have the "Async" suffix stripped from the generated Typescript.

    Method Annotations

    Methods can be annotated with attributes to control API exposure and TypeScript generation. The following attributes are available for model methods. General annotations can be found on the Attributes page.

    [Coalesce]

    The [Coalesce] attribute causes the method to be exposed via a generated API controller. This is not needed for methods defined on an interface marked with [Service] - Coalesce assumes that all methods on the interface are intended to be exposed. If this is not desired, create a new, more restricted interface with only the desired methods to be exposed.

    [ControllerAction(Method = HttpMethod, VaryByProperty = string)]

    The [ControllerAction] attribute controls how this method is exposed via HTTP. Can be used to customize the HTTP method/verb for the method, as well as caching behavior.

    [Execute(string roles)]

    The [Execute] attribute specifies which roles can execute this method from the generated API controller.

    [LoadFromDataSource(Type dataSourceType)]

    The [LoadFromDataSource] attribute specifies that the targeted model instance method should load the instance it is called on from the specified data source when invoked from an API endpoint. By default, the default data source for the model's type will be used.

    File Downloads

    Coalesce supports exposing file downloads via custom methods. Simply return a IntelliTect.Coalesce.Models.IFile (or any derived type, like IntelliTect.Coalesce.Models.File), or an ItemResult<> of such.

    Consuming file downloads

    There are a few conveniences for easily consuming downloaded files from your custom pages.

    The API Callers have a property url. This can be provided directly to your HTML template, with the browser invoking the endpoint automatically.

    import { PersonViewModel } from '@/viewmodels.g'
    +
    +var viewModel = new PersonViewModel();
    +viewModel.$load(1);
    +
    <img :src="downloadPicture.url">
    +

    Alternatively, the API Callers for file-returning methods have a method getResultObjectUrl(vue). If the method was invoked programmatically (i.e. via caller(), caller.invoke(), or caller.invokeWithArgs()), this method returns an Object URLopen in new window that can be set as the src of an image or video HTML tag.

    import { PersonViewModel } from '@/viewmodels.g'
    +
    +var viewModel = new PersonViewModel();
    +await viewModel.$load(1);
    +await viewModel.downloadPicture();
    +
    <img :src="downloadPicture.getResultObjectUrl()">
    +

    Database-stored Files

    When storing large byte[] objects in your EF models, it is important that these are never loaded unless necessary. Loading these can cause significant garbage collector churn, or even bring your app to a haltopen in new window. To achieve this with EF, you can either utilize Table Splittingopen in new window, or you can use an entire dedicated table that only contains a primary key and the binary content, and nothing else.

    WARNING

    Storing large binary objects in relational databases comes with significant drawbacks. For large-volume cloud solutions, it is much more costly than dedicated cloud-native file storage like Azure Storage or S3. Also of note is that the larger a database is, the more difficult its backup process becomes.

    For files that are stored in your database, Coalesce supports a pattern that allows the file to be streamed directly to the HTTP response without needing to allocate a chunk of memory for the whole file at once. Simply pass an EF IQueryable<byte[]> to the constructor of IntelliTect.Coalesce.Models.File. This implementation, however, is specific to the underlying EF database provider. Currently, only SQL Server and SQLite are supported. Please open a Github issue to request support for other providers. An example of this mechanism is included in the DownloadAttachment method in the code sample below.

    The following is an example of utilizing Table Splitting for database-stored files. Generally speaking, metadata about the file should be stored on the "main" entity, and only the bytes of the content should be split into a separate entity.

    public class AppDbContext : DbContext
    +{
    +    public DbSet<Case> Cases { get; set; }
    +
    +    protected override void OnModelCreating(ModelBuilder modelBuilder)
    +    {
    +        modelBuilder
    +            .Entity<Case>()
    +            .ToTable("Cases")
    +            .HasOne(c => c.AttachmentContent)
    +            .WithOne()
    +            .HasForeignKey<CaseAttachmentContent>(c => c.CaseId);
    +        modelBuilder
    +            .Entity<CaseAttachmentContent>()
    +            .ToTable("Cases")
    +            .HasKey(d => d.CaseId);
    +    }
    +}
    +
    +public class Case
    +{
    +    public int CaseId { get; set; }
    +
    +    [Read]
    +    public string AttachmentName { get; set; }
    +
    +    [Read]
    +    public long AttachmentSize { get; set; }
    +
    +    [Read]
    +    public string AttachmentType { get; set; }
    +
    +    [Read, MaxLength(32)] // Adjust max length based on chosen hash algorithm.
    +    public byte[] AttachmentHash { get; set; } // Could also be a base64 string if so desired.
    +
    +    [InternalUse]
    +    public CaseAttachmentContent AttachmentContent { get; set; } = new();
    +
    +    [Coalesce]
    +    public async Task UploadAttachment(AppDbContext db, IFile file)
    +    {
    +        if (file.Content == null) return;
    +
    +        var content = new byte[file.Length];
    +        await file.Content.ReadAsync(content.AsMemory());
    +
    +        AttachmentContent = new () { CaseId = CaseId, Content = content };
    +        AttachmentName = file.Name;
    +        AttachmentSize = file.Length;
    +        AttachmentType = file.ContentType;
    +        AttachmentHash = SHA256.HashData(content);
    +    }
    +
    +    [Coalesce]
    +    [ControllerAction(HttpMethod.Get, VaryByProperty = nameof(AttachmentHash))]
    +    public IFile DownloadAttachment(AppDbContext db)
    +    {
    +        return new IntelliTect.Coalesce.Models.File(db.Cases
    +            .Where(c => c.CaseId == this.CaseId)
    +            .Select(c => c.AttachmentContent.Content)
    +        )
    +        {
    +            Name = AttachmentName,
    +            ContentType = AttachmentType,
    +        };
    +    }
    +}
    +
    +public class CaseAttachmentContent
    +{
    +    public int CaseId { get; set; }
    +
    +    [Required]
    +    public byte[] Content { get; set; }
    +}
    +

    Other File Storage

    For any other storage mechanism, implementations are similar to the database storage approach above. However, instead of table splitting or using a whole separate table, the file contents are simply stored elsewhere. Continue storing metadata about the file on the primary entity, and implement upload/download methods as desired that wrap the storage provider.

    For downloads, prefer directly providing the underlying Stream to the IFile versus wrapping a byte[] in a MemoryStream. This will reduce server memory usage and garbage collector churn.

    For cloud storage providers where complex security logic is not needed, consider having clients consume the URL of the cloud resource directly rather than passing the file content through your own server.

    + + + diff --git a/modeling/model-components/properties.html b/modeling/model-components/properties.html new file mode 100644 index 000000000..5564d82ed --- /dev/null +++ b/modeling/model-components/properties.html @@ -0,0 +1,33 @@ + + + + + + + + + Properties | Coalesce + + + + +

    Properties

    Models in a Coalesce application are just EF Core POCOs. The properties defined on your models should fit within the constraints of EF Core.

    Coalesce currently has a few more restrictions than what EF Core allows, but hopefully over time some of these restrictions can be relaxed as Coalesce grows in capability.

    Property Varieties

    The following kinds of properties may be declared on your models.

    Primary Key

    To work with Coalesce, your model must have a single property for a primary key. By convention, this property should be named the same as your model class with Id appended to that name, but you can also annotate a property with [Key] to denote it as the primary key.

    Foreign Keys & Reference Navigation Properties

    While a foreign key may be declared on your model using only the EF OnModuleBuilding method to specify its purpose, Coalesce won't know what the property is a key for. Therefore, foreign key properties should always be accompanied by a reference navigation property, and vice versa.

    In cases where the foreign key is not named after the navigation property with "Id" appended, the [ForeignKeyAttribute] may be used on either the key or the navigation property to denote the other property of the pair, in accordance with the recommendations set forth by EF Core's Modeling Guidelinesopen in new window.

    Collection Navigation Properties

    Collection navigation properties can be used in a straightforward manner. In the event where the inverse property on the other side of the relationship cannot be determined, [InversePropertyAttribute] will need to be used. EF Core provides documentationopen in new window on how to use this attribute. Errors will be displayed at generation time if an inverse property cannot be determined without the attribute. We recommend recommended that you declare the type of collection navigation properties as ICollection<T>.

    Non-mapped POCOs

    Properties of a type that are not on your DbContext will also have corresponding properties generated on the TypeScript ViewModels typed as TypeScript External ViewModels, and the values of such properties will be sent with the object to the client when requested. Properties of this type will also be sent back to the server by the client when they are encountered (currently supported by the Vue Stack only).

    See External Types for more information.

    Primitives, Scalars, & Dates

    Most common built-in primitive and scalar data types (numerics, strings, booleans, enums, DateTime, DateTimeOffset), and their nullable variants, are all supported as model properties.

    Getter-only Properties

    Any property that only has a getter will also have a corresponding property generated in the TypeScript ViewModels, but won't be sent back to the server during any save actions.

    If such a property is defined as an auto-property, the [NotMapped] attribute should be used to prevent EF Core from attempting to map such a property to your database.

    Init-only Properties

    Properties on Entity Models that use an init accessor rather than a set accessor will be implicitly treated as required, and can also only have a value provided when the entity is created for the first time. Any values provided during save actions for init-only properties when updating an existing entity will be ignored.

    Other Considerations

    For any of the kinds of properties outlined above, the following rules are applied:

    Attributes

    Coalesce provides a number of Attributes, and supports a number of other .NET attributes, that allow for further customization of your model.

    Security

    Properties will be ignored if received by the client if authorization checks against any property-level Security present fail. This security is handled by the Generated C# DTOs.

    Loading & Serialization

    The Default Loading Behavior, any custom functionality defined in Data Sources, and [DtoIncludes] & [DtoExcludes] may also restrict which properties are sent to the client when requested.

    NotMapped

    While Coalesce does not do anything special for the [NotMapped] attribute, it is still and important attribute to keep in mind while building your model, as it prevents EF Core from doing anything with the property.

    + + + diff --git a/modeling/model-types/dtos.html b/modeling/model-types/dtos.html new file mode 100644 index 000000000..85d29ff17 --- /dev/null +++ b/modeling/model-types/dtos.html @@ -0,0 +1,123 @@ + + + + + + + + + Custom DTOs | Coalesce + + + + +

    Custom DTOs

    In addition to the generated Generated C# DTOs that Coalesce will create for you, you may also create your own implementations of an IClassDto. These types are first-class citizens in Coalesce - you will get a full suite of features surrounding them as if they were entities. This includes generated API Controllers, Admin Views, and full TypeScript ViewModels and TypeScript List ViewModels.

    The difference between a Custom DTO and the underlying entity that they represent is as follows:

    • The only time your custom DTO will be served is when it is requested directly from one of the endpoints on its generated controller, or when its type is explicitly used by a method or property of another type.

    • When mapping data from your database, or mapping data incoming from the client, the DTO itself must manually map all properties, since there is no corresponding Generated DTO. Attributes like [DtoIncludes] & [DtoExcludes] and property-level security through Security Attributes have no effect on custom DTOs, since those attribute only affect what get generated for Generated C# DTOs.

    Creating a Custom DTO

    To create a custom DTO, define a class annotated with [Coalesce] that implements IClassDTO<T>, where T is an EF Core POCO with a corresponding DbSet<T> on a DbContext that has also been exposed with [Coalesce]. Add any Properties to it just as you would add model properties to a regular EF model. If you are not exposing a DbContext class with [Coalesce] but still wish to create a Custom DTO based upon one of its entities, you can inherit from IClassDTO<T, TContext> instead as a means of explicitly declaring the type of the DbContext.

    Next, ensure that one property is annotated with [Key] so that Coalesce can know the primary key of your DTO in order to perform database lookups and keep track of your object uniquely in the client-side TypeScript.

    Now, populate the required MapTo and MapFrom methods with code for mapping from and to your DTO, respectively (the methods are named with respect to the underlying entity, not the DTO). Most properties probably map one-to-one in both directions, but you probably created a DTO because you wanted some sort of custom mapping - say, mapping a collection on your entity with a comma-delimited string on the DTO. This is also the place to perform any user-based, role-based, property-level security. You can access the current user on the IMappingContext object.

    [Coalesce]
    +public class CaseDto : IClassDto<Case>
    +{
    +    [Key]
    +    public int CaseId { get; set; }
    +
    +    public string Title { get; set; }
    +
    +    [Read]
    +    public string AssignedToName { get; set; }
    +
    +    public void MapTo(Case obj, IMappingContext context)
    +    {
    +        obj.Title = Title;
    +    }
    +
    +    public void MapFrom(Case obj, IMappingContext context = null, IncludeTree tree = null)
    +    {
    +        CaseId = obj.CaseKey;
    +        Title = obj.Title;
    +        if (obj.AssignedTo != null)
    +        {
    +            AssignedToName = obj.AssignedTo.Name;
    +        }
    +    }
    +}
    +

    WARNING

    Custom DTOs do not utilize property-level Security Attributes nor [DtoIncludes] & [DtoExcludes], since these are handled in the Generated DTOs. If you need property-level security or trimming, you must write it yourself in the MapTo and MapFrom methods.

    If you have any child objects on your DTO, you can invoke the mapper for some other object using the static Mapper class. Also seen in this example is how to respect the Include Tree when mapping entity types; however, respecting the IncludeTree is optional. Since this DTO is a custom type that you've written, if you're certain your use cases don't need to worry about object graph trimming, then you can ignore the IncludeTree. If you do ignore the IncludeTree, you should pass null to calls to Mapper - don't pass in the incoming IncludeTree, as this could cause unexpected results.

    using IntelliTect.Coalesce.Mapping;
    +
    +[Coalesce]
    +public class CaseDto : IClassDto<Case>
    +{
    +    public int ProductId { get; set; }
    +    public Product Product { get; set; }
    +    ...
    +
    +    public void MapFrom(Case obj, IMappingContext context = null, IncludeTree tree = null)
    +    {
    +        ProductId = obj.ProductId;
    +
    +        if (tree == null || tree[nameof(this.Product)] != null)
    +            Product = Mapper.MapToDto<Product, ProductDtoGen>(obj.Product, context, tree?[nameof(this.Product)]
    +        ...
    +    }
    +}
    +

    Using Custom DataSources and Behaviors

    Declaring an IClassDto DataSource

    When you create a custom DTO, it will use the Standard Data Source and Standard Behaviors just like any of your regular Entity Models. If you wish to override this, your custom data source and/or behaviors MUST be declared in one of the following ways:

    1. As a nested class of the DTO. The relationship between your data source or behaviors and your DTO will be picked up automatically.

      [Coalesce]
      +public class CaseDto : IClassDto<Case>
      +{
      +    [Key]
      +    public int CaseId { get; set; }
      +
      +    public string Title { get; set; }
      +    
      +    ...
      +
      +    public class MyCaseDtoSource : StandardDataSource<Case, AppDbContext>
      +    {
      +        ...
      +    }
      +}
      +
    2. With a [DeclaredFor] attribute that references the DTO type:

      [Coalesce]
      +public class CaseDto : IClassDto<Case>
      +{
      +    [Key]
      +    public int CaseId { get; set; }
      +
      +    public string Title { get; set; }
      +    
      +    ...
      +}
      +
      +[Coalesce, DeclaredFor(typeof(CaseDto))]
      +public class MyCaseDtoSource : StandardDataSource<Case, AppDbContext>
      +{
      +    ...
      +}
      +

    ProjectedDtoDataSource

    In addition to creating a Data Source by deriving from Standard Data Source, there also exists a class ProjectedDtoDataSource that can be used to easily perform projection from EF model types to your custom DTO types using EF query projections. ProjectedDtoDataSource inherits from Standard Data Source.

    [Coalesce, DeclaredFor(typeof(CaseDto))]
    +public class CaseDtoSource : ProjectedDtoDataSource<Case, CaseDto, AppDbContext>
    +{
    +    public CaseDtoSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<CaseDto> ApplyProjection(IQueryable<Case> query, IDataSourceParameters parameters)
    +    {
    +        return query.Select(c => new CaseDto
    +        {
    +            CaseId = c.CaseKey,
    +            Title = c.Title,
    +            AssignedToName = c.AssignedTo == null ? null : c.AssignedTo.Name
    +        });
    +    }
    +}
    +

    Surgical Saves

    The Vue ViewModels support surgical saves through their $saveMode property, and Knockout ViewModels through the saveIncludedFields configuration.

    Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the x-www-form-urlencoded body that is sent to the server.

    The Generated C# DTOs implement the necessary logic for this; however, any Custom DTOs must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the Generated C# DTOs, or do not use surgical saves with Custom DTOs.

    + + + diff --git a/modeling/model-types/entities.html b/modeling/model-types/entities.html new file mode 100644 index 000000000..e3e10a616 --- /dev/null +++ b/modeling/model-types/entities.html @@ -0,0 +1,81 @@ + + + + + + + + + Entity Models | Coalesce + + + + +

    Entity Models

    Models are the core business objects of your application - they serve as the fundamental representation of data in your application. The design of your models is very important. In Entity Framework Coreopen in new window (EF), data models are just Plain Old CLR Objects (POCOs).

    Building a Data Model

    To start building your data model that Coalesce will generate code for, follow the best practices for EF Coreopen in new window. Guidance on this topic is available in abundance in the Entity Framework Core documentationopen in new window.

    Don't worry about querying or saving data when you're just getting started - Coalesce will provide a lot of that functionality for you, and it is very easy to customize what Coalesce offers later. To get started, just build your POCOs and DbContext classes. Annotate your DbContext class with [Coalesce] so that Coalesce will discover it and generate code based off of your context for you.

    Before you start building, you are highly encouraged to read the sections below. The linked pages explain in greater detail what Coalesce will build for you for each part of your data model.

    Properties

    Read Properties for an outline of the different types of properties that you may place on your models and the code that Coalesce will generate for each of them.

    Attributes

    Coalesce provides a number of C# attributes that can be used to decorate your model classes and their properties in order to customize behavior, appearance, security, and more. Coalesce also supports a number of annotations from System.ComponentModel.DataAnnotations.

    Read Attributes to learn more.

    Methods

    You can place both static and interface methods on your model classes. Any public methods annotated with [Coalesce] will have a generated API endpoint and corresponding generated TypeScript members for calling this API endpoint. Read Methods to learn more.

    Customizing CRUD Operations

    Once you've got a solid data model in place, its time to start customizing the way that Coalesce will read your data, as well as the way that it will handle your data when processing creates, updates, and deletes.

    Data Sources

    The method by which you can control what data the users of your application can access through Coalesce's generated APIs is by creating custom data sources. These are classes that allow complete control over the way that data is retrieved from your database and provided to clients. Read Data Sources to learn more.

    Behaviors

    Behaviors in Coalesce are to mutating data as data sources are to reading data. Defining a behaviors class for a model allows complete control over the way that Coalesce will create, update, and delete your application's data in response to requests made through its generated API. Read Behaviors to learn more.

    Standalone (non-EF) Entities

    In Coalesce, Standalone Entities are entity types that are not based on Entity Framework. These types are discovered by Coalesce by annotating them with [Coalesce, StandaloneEntity].

    For these types, you must define at least one custom Data Source, and optionally a Behaviors class as well. If no behaviors are defined, the type is implicitly read-only, equivalent to turning off create/edit/delete via the Security Attributes.

    To define data sources and behaviors for Standalone Entities, it is recommended you inherit from StandardDataSource<T> and StandardBehaviors<T>, respectively. For example:

    [Coalesce, StandaloneEntity]
    +public class StandaloneExample
    +{
    +    public int Id { get; set; }
    +
    +    [Search(SearchMethod = SearchAttribute.SearchMethods.Contains), ListText]
    +    public string Name { get; set; } = "";
    +
    +    [DefaultOrderBy]
    +    public DateTimeOffset Date { get; set; }
    +
    +    private static int nextId = 0;
    +    private static ConcurrentDictionary<int, StandaloneExample> backingStore = new ConcurrentDictionary<int, StandaloneExample>();
    +
    +    public class DefaultSource : StandardDataSource<StandaloneExample>
    +    {
    +        public DefaultSource(CrudContext context) : base(context) { }
    +
    +        public override Task<IQueryable<StandaloneExample>> GetQueryAsync(IDataSourceParameters parameters)
    +            => Task.FromResult(backingStore.Values.AsQueryable());
    +    }
    +
    +    public class Behaviors : StandardBehaviors<StandaloneExample>
    +    {
    +        public Behaviors(CrudContext context) : base(context) { }
    +
    +        public override Task ExecuteDeleteAsync(StandaloneExample item)
    +        {
    +            backingStore.TryRemove(item.Id, out _);
    +            return Task.CompletedTask;
    +        }
    +
    +        public override Task ExecuteSaveAsync(SaveKind kind, StandaloneExample? oldItem, StandaloneExample item)
    +        {
    +            if (kind == SaveKind.Create)
    +            {
    +                item.Id = Interlocked.Increment(ref nextId);
    +                backingStore.TryAdd(item.Id, item);
    +            }
    +            else
    +            {
    +                backingStore.TryRemove(item.Id, out _);
    +                backingStore.TryAdd(item.Id, item);
    +            }
    +            return Task.CompletedTask;
    +        }
    +    }
    +}
    +

    The above example is admittedly contrived, as it is unlikely that you would be using an in-memory collection as a data persistence mechanism. A more likely real-world scenario would be to inject an interface to some other data store. Data Source and Behavior classes are instantiated using your application's service provider, so any registered service can be injected.

    + + + diff --git a/modeling/model-types/external-types.html b/modeling/model-types/external-types.html new file mode 100644 index 000000000..48fdf2c07 --- /dev/null +++ b/modeling/model-types/external-types.html @@ -0,0 +1,74 @@ + + + + + + + + + External Types | Coalesce + + + + +

    External Types

    In Coalesce, any type which is connected to your data model but is not directly part of it is considered to be an "external type".

    The collection of external types for a data model looks like this:

    1. Take all of the api-served types in your data model. This includes Entity Models and Custom DTOs.
    2. Take all of the property types, method parameters, and method return types of these types.
    3. Any of these types which are not built-in scalar types and not one of the aforementioned api-served types are external types.
    4. For any external type discovered, any of the property types which qualify under the above rules are also external types.

    WARNING

    Be careful when using types that you do not own for properties and method returns in your data model. When Coalesce generates external type ViewModels and DTOs, it will not stop until it has exhausted all paths that can be reached by following public property types and method returns.

    In general, you should only expose types that you have created so that you will always have full control over them. Mark any properties you don't wish to expose with [InternalUse], or make those members non-public.

    Generated Code

    For each external type found in your application's model, Coalesce will generate:

    Example Data Model

    For example, in the following scenario, the following classes are considered as external types:

    • PluginMetadata, exposed through a getter-only property on ApplicationPlugin.
    • PluginResult, exposed through a method return on ApplicationPlugin.

    PluginHandler is not because it not exposed by the model, neither directly nor through any of the other external types.

    public class AppDbContext : DbContext {
    +    public DbSet<Application> Applications { get; set; }
    +    public DbSet<ApplicationPlugin> ApplicationPlugins { get; set; }
    +}
    +
    +public class Application {
    +    public int ApplicationId { get; set; }
    +    public string Name { get; set; }
    +    public ICollection<ApplicationPlugin> Plugins { get; set; }
    +}
    +
    +public class ApplicationPlugin {
    +    public int ApplicationPluginId { get; set; }
    +    public int ApplicationId { get; set; }
    +    public Application Application { get; set; }
    +
    +    public string TypeName { get; set; }
    +
    +    private PluginHandler GetInstance() => 
    +        ((PluginHandler)Activator.CreateInstance(Type.GetType(TypeName)));
    +
    +    public PluginMetadata Metadata => GetInstance().GetMetadata();
    +
    +    public PluginResult Invoke(string action, string data) => GetInstance().Invoke(Application, action, data);
    +}
    +
    +public abstract class PluginHandler { 
    +    public abstract PluginMetadata GetMetadata();
    +    public abstract PluginResult Invoke(Application app, string action, string data);
    +}
    +
    +public abstract class PluginMetadata { 
    +    public bool Name { get; set; }
    +    public string Version { get; set; }
    +    public ICollection<string> Actions { get; set; }
    +}
    +
    +public abstract class PluginResult { 
    +    public bool Success { get; set; }
    +    public string Message { get; set; }
    +}
    +

    Loading & Serialization

    External types have slightly different behavior when undergoing serialization to be sent to the client. Unlike database-mapped types which are subject to the rules of Include Tree, external types ignore the Include Tree when being mapped to DTOs for serialization. Read External Type Caveats for a more detailed explanation of this exception.

    + + + diff --git a/modeling/model-types/services.html b/modeling/model-types/services.html new file mode 100644 index 000000000..68bf0416b --- /dev/null +++ b/modeling/model-types/services.html @@ -0,0 +1,62 @@ + + + + + + + + + Services | Coalesce + + + + +

    Services

    In a Coalesce, you are fairly likely to end up with a need for some API endpoints that aren't closely tied with your regular data model. While you could stick static Methods on one of your entities, this solution just leads to a jumbled mess of functionality all over your data model that doesn't belong there.

    Instead, Coalesce allows you to generate API Controllers and a TypeScript client from a service. A service, in this case, is nothing more than a C# class or an interface with methods on it, annotated with [Coalesce,Service]. An implementation of this class or interface must be injectable from your application's service container, so a registration in Startup.cs is needed.

    The instance methods of these services work just like other custom Methods in Coalesce, with one notable distinction: Instance methods don't operate on an instance of a model, but instead on a dependency injected instance of the service.

    Generated Code

    For each external type found in your application's model, Coalesce will generate:

    • An API controller with endpoints that correspond to the service's instance methods.
    • A TypeScript client containing the members outlined in Methods for invoking these endpoints.

    Example Service

    An example of a service might look something like this:

    [Coalesce, Service]
    +public interface IWeatherService
    +{
    +    WeatherData GetWeather(string zipCode);
    +}
    +

    With an implementation:

    public class WeatherService : IWeatherService
    +{
    +    public WeatherService(AppDbContext db)
    +    {
    +        this.db = db;
    +    }
    +
    +    public WeatherData GetWeather(string zipCode)
    +    {
    +        // Assuming some magic HttpGet method that works as follows...
    +        var response = HttpGet("http://www.example.com/api/weather/" + zipCode);
    +        return response.Body.SerializeTo<WeatherData>();
    +    }
    +
    +    public void MethodThatIsNotExposedBecauseItIsNotOnTheExposedInterface() {  }
    +}
    +

    And a registration:

    public class Startup 
    +{
    +    public void ConfigureServices(IServiceCollection services)
    +    {
    +        services.AddCoalesce<AppDbContext>();
    +        services.AddScoped<IWeatherService, WeatherService>();
    +    }
    +}
    +

    While it isn't required that an interface for your service exist - you can generate directly from the implementation, it is highly recommended that an interface be used. Interfaces increase testability and reduce risk of accidentally changing the signature of a published API, among other benefits.

    + + + diff --git a/stacks/agnostic/dtos.html b/stacks/agnostic/dtos.html new file mode 100644 index 000000000..db503b99e --- /dev/null +++ b/stacks/agnostic/dtos.html @@ -0,0 +1,33 @@ + + + + + + + + + Generated C# DTOs | Coalesce + + + + +

    Generated C# DTOs

    Data Transfer Objects, or DTOs, allow for transformations of data from the data store into a format more suited for transfer and use on the client side. This often means trimming properties and flattening structures to provide a leaner over-the-wire experience. Coalesce aims to support this as seamlessly as possible.

    Coalesce supports two types of DTOs:

    • DTOs that are automatically generated for each POCO database object. These are controlled via Attributes on the POCO. These are outlined below.
    • DTOs that you create with IClassDto. These are outlined at Custom DTOs.

    Automatically Generated DTOs

    Every class that is exposed through Coalesce's generated API will have a corresponding DTO generated for it. These DTOs are used to shuttle data back and forth to the client. They are generated classes that have nullable versions of all the properties on the POCO class.

    [DtoIncludes] & [DtoExcludes] and the Includes String infrastructure can be used to indicate which properties should be transferred to the client in which cases, and Include Tree is used to dictate how these DTOs are constructed from POCOs retrieved from the database.

    The [Read] and [Edit] attributes can be used to apply property-level security, which manifests as conditional logic in the mapping methods on the generated DTOs.

    See the Security page to read more about property-level security, as well as all other security mechanisms in Coalesce.

    + + + diff --git a/stacks/agnostic/generation.html b/stacks/agnostic/generation.html new file mode 100644 index 000000000..2f0e81eda --- /dev/null +++ b/stacks/agnostic/generation.html @@ -0,0 +1,51 @@ + + + + + + + + + Code Generation Overview | Coalesce + + + + +

    Code Generation Overview

    Coalesce's principal purpose is a code generation framework for automating the creation of the boring-but-necessary parts of a web application. Below, you find an overview of the different components of Coalesce's code generation features.

    Running Code Generation

    Coalesce's code generation is ran via a dotnet CLI tool, dotnet coalesce. In order to invoke this tool, you must have the appropriate references to the package that provides it in your .csproj file:

    <Project Sdk="Microsoft.NET.Sdk.Web">
    +  <PropertyGroup>
    +    <TargetFramework>net7.0</TargetFramework>
    +
    +    <!-- Necessary to use DotNetCliToolReference with modern framework versions -->
    +    <DotnetCliToolTargetFramework>net7.0</DotnetCliToolTargetFramework>
    +  </PropertyGroup>
    +
    +  ...
    +
    +  <ItemGroup>
    +    <PackageReference Include="IntelliTect.Coalesce" Version="..." />
    +  </ItemGroup>
    +
    +  <ItemGroup>
    +    <DotNetCliToolReference Include="IntelliTect.Coalesce.Tools" Version="..." />
    +  </ItemGroup>  
    +</Project>
    +

    CLI Options

    All configuration of the way that Coalesce interacts with your projects, including locating, analyzing, and producing generated code, is done in a json configuration file, coalesce.json. Read more about this file at Code Generation Configuration.

    There are a couple of extra options which are only available as CLI parameters to dotnet coalesce. These options do not affect the behavior of the code generation - only the behavior of the CLI itself.

    --debug - When this flag is specified when running dotnet coalesce, Coalesce will wait for a debugger to be attached to its process before starting code generation.

    -v|--verbosity <level> - Set the verbosity of the output. Options are trace, debug, information, warning, error, critical, and none.

    Generated Code

    Coalesce will generate a full vertical stack of code for you:

    Backend C#

    API Controllers

    For each of your Entity Models, Custom DTOs, and Services, an API controller is created in the /Api/Generated directory of your web project. These controllers provide a number of endpoints for interacting with your data.

    These controllers can be secured at a high level using Security Attributes, and when applicable to the type, with Data Sources and Behaviors.

    C# DTOs

    For each of your Entity Models, a C# DTO class is created. These classes are used to hold the data that will be serialized and sent to the client, as well as data that has been received from the client before it has been mapped back to your EF POCO class.

    See Generated C# DTOs for more information.

    Frontend - Vue

    An overview of the Vue generated code can be found at Vue Overview.

    Frontend - Knockout

    An overview of the legacy Knockout generated code can be found at Knockout Overview.

    + + + diff --git a/stacks/agnostic/getting-started-modeling.html b/stacks/agnostic/getting-started-modeling.html new file mode 100644 index 000000000..21a2f6427 --- /dev/null +++ b/stacks/agnostic/getting-started-modeling.html @@ -0,0 +1,33 @@ + + + + + + + + + Data Modeling | Coalesce + + + + +

    Data Modeling

    At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:

    • Create an initial Data Model by adding EF entity classes to the data project and the corresponding DbSet<> properties to AppDbContext. You will notice that the starter project includes a single model, ApplicationUser, to start with. Feel free to change this model or remove it entirely. Read Entity Models for more information about creating a data model.

    • Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.

    • Run Coalesce's code generation by either:

      • Running dotnet coalesce in the web project's root directory
      • Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer

    You're now at a point where you can start creating your own pages!

    + + + diff --git a/stacks/disambiguation/external-view-model.html b/stacks/disambiguation/external-view-model.html new file mode 100644 index 000000000..d0b0f1fdf --- /dev/null +++ b/stacks/disambiguation/external-view-model.html @@ -0,0 +1,33 @@ + + + + + + + + + TypeScript External ViewModels | Coalesce + + + + +

    TypeScript External ViewModels

    This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.

    Vue

    The Vue stack for Coalesce does not create dedicated ViewModels for External Types. The interfaces in the Model Layer are used as the only representation of External Types on the client.

    Knockout

    See: TypeScript External ViewModels

    + + + diff --git a/stacks/disambiguation/list-view-model.html b/stacks/disambiguation/list-view-model.html new file mode 100644 index 000000000..610ed277f --- /dev/null +++ b/stacks/disambiguation/list-view-model.html @@ -0,0 +1,33 @@ + + + + + + + + + TypeScript List ViewModels | Coalesce + + + + +

    TypeScript List ViewModels

    This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.

    Vue

    See: Vue ListViewModels

    Knockout

    See: Knockout ListViewModels

    + + + diff --git a/stacks/disambiguation/view-model.html b/stacks/disambiguation/view-model.html new file mode 100644 index 000000000..25e1e04eb --- /dev/null +++ b/stacks/disambiguation/view-model.html @@ -0,0 +1,33 @@ + + + + + + + + + TypeScript ViewModels | Coalesce + + + + +

    TypeScript ViewModels

    This is a disambiguation page for a concept in Coalesce that is implemented differently between the available front-end stack choices. Please select your preferred stack.

    Vue

    See: Vue ViewModels

    Knockout

    See: Knockout ViewModels

    + + + diff --git a/stacks/ko/client/bindings.html b/stacks/ko/client/bindings.html new file mode 100644 index 000000000..5756fb41e --- /dev/null +++ b/stacks/ko/client/bindings.html @@ -0,0 +1,86 @@ + + + + + + + + + Knockout Bindings | Coalesce + + + + +

    Knockout Bindings

    Coalesce provides a number of knockout bindings that make common model binding activities much easier.

    Editors Note: On this page, some bindings are split into their requisite HTML component with their data-bind component listed immediately after. Keep this in mind when reading.

    Input Bindings

    select2Ajax

    <select data-bind="
    +    select2Ajax: personId, 
    +    url: '/api/Person/list', 
    +    idField: 'personId', 
    +    textField: 'Name', 
    +    object: person, 
    +    allowClear: true
    +"></select>
    +

    Creates a select2 dropdown using the specified url and fields that can be used to select an object from the endpoint specified. Additional complimentary bindings include:

    url: string

    The Coalesce List API url to call to populate the contents of the dropdown.

    idField: string

    The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set on the observable specified for the main select2Ajax binding.

    textField: string

    The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.

    object?: KnockoutObservable<Coalesce.BaseViewModel | null>

    An observable that holds the full object corresponding to the foreign key property being bound to. If the selected value changes, this will be set to null to avoid representation of incorrect data (unless setObject is used - see below).

    setObject: boolean = false

    If true, the observable specified by the object binding will be set to the selected data when an option is chosen in the dropdown. Binding itemViewModel is required if this binding is set.

    Additionally, requests to the API to populate the dropdown will request the entire object, as opposed to only the two fields specified for idField and textField like is normally done when this binding is missing or set to false. To override this behavior and continue requesting only specific fields even when setObject is true, add fields=field1,field2,... to the query string of the url binding.

    itemViewModel?: (new (newItem: object) => Coalesce.BaseViewModel)

    A reference to the class that represents the type of the object held in the object observable. This is used when constructing new objects from the results of the API call. Not used if setObject is false or unspecified. For example, setObject: true, itemViewModel: ViewModels.Person.

    pageSize: number = 25

    The number of items to request in each call to the server.

    format: string = '{0}'

    A string containing the substring {0}, which will be replaced with the text value of an option in the dropdown list when the option is displayed.

    selectionFormat: string = '{0}'

    A string containing the substring {0}, which will be replaced with the text value of the selected option of the dropdown list.

    cache: boolean = true

    If true, a cache-busting querystring parameter will be included in AJAX requests.

    selectOnClose: boolean = false

    Directly maps to select2 option selectOnClose.

    allowClear: boolean = true

    Whether or not to allow the current select to be set to null. Directly maps to select2 option allowClear.

    placeholder: string = 'select'

    Placeholder when nothing is selected. Directly maps to select2 option placeholder.

    openOnFocus: boolean = false

    If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

    select2AjaxMultiple

    <select multiple="multiple" data-bind="
    +    select2AjaxMultiple: people, 
    +    url: '/api/Person/list', 
    +    idField: 'personId', 
    +    textField: 'Name', 
    +    itemViewModel: ViewModels.PersonCase
    +"></select>
    +

    Creates a select2 multi-select input for choosing objects that participate as the foreign object in a many-to-many relationship with the current object. The primary select2AjaxMultiple binding takes the collection of items that make up the foreign side of the relationship. This is NOT the collection of the join objects (a.k.a. middle table objects) in the relationship.

    Additional complimentary bindings include:

    url: string

    The Coalesce List API url to call to populate the contents of the dropdown. In order to only receive specific fields from the server, add fields=field1,field2,... to the query string of the url, ensuring that at least the idField and textField are included in that collection.

    idField: string

    The name of the field on each item in the results of the AJAX call which contains the ID of the option. The value of this field will be set as the key of the foreign object in the many-to-many relationship.

    textField: string

    The name of the field on each item in the results of the AJAX call which contains the text to be displayed for each option.

    itemViewModel: (new (newItem: object) => Coalesce.BaseViewModel)

    A reference to the class that represents the types in the supplied collection. For example, a many-to-many between Person and Case objects where Case is the object being bound to and Person is the type represented by a child collection, the correct value is ViewModels.Person. This is used when constructing new objects representing the relationship when a new item is selected.

    pageSize: number = 25

    The number of items to request in each call to the server.

    format: string = '{0}'

    A string containing the substring {0}, which will be replaced with the text value of an option in the dropdown list when the option is displayed.

    selectionFormat: string = '{0}'

    A string containing the substring {0}, which will be replaced with the text value of the selected option of the dropdown list.

    cache: boolean = true

    If true, a cache-busting querystring parameter will be included in AJAX requests.

    selectOnClose: boolean = false

    Directly maps to select2 option selectOnClose.

    allowClear: boolean = true

    Whether or not to allow the current select to be set to null. Directly maps to select2 option allowClear.

    placeholder: string = 'select'

    Placeholder when nothing is selected. Directly maps to select2 option placeholder.

    openOnFocus: boolean = false

    If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

    select2AjaxText

    <select data-bind="
    +    select2AjaxText: schoolName, 
    +    url: '/api/Person/SchoolNames'
    +"></select>
    +

    Creates a select2 dropdown against the specified url where the url returns a collection of string values that are potential selection candidates. The dropdown also allows the user to input any value they choose - the API simply serves suggested values.

    url: string

    The url to call to populate the contents of the dropdown. This should be an endpoint that returns one of the following:

    • A raw string[]
    • An object that conforms to { list: string[] }
    • An object that conforms to { object: string[] }
    • An object that conforms to { list: { [prop: string]: string } } where the value given to resultField is a valid property of the returned objects.
    • An object that conforms to { object: { [prop: string]: string } } where the value given to resultField is a valid property of the returned objects.

    The url will also be passed a search parameter and a page parameter appended to the query string. The chosen endpoint is responsible for implementing this functionality. Page size is expected to be some fixed value. Implementer should anticipate that the requested page may be out of range.

    The cases listed above that accept arrays of objects (as opposed to arrays of strings) require that the resultField binding is also used. These are designed for obtaining string values from objects obtained from the standard list endpoint.

    resultField?: string

    If provided, specifies a field on the objects returned from the API to pull the string values from. See examples in url above.

    allowCustom: boolean = true

    If false, the user's search input will not be presented as a valid selectable value; only the exact values obtained from the API endpoint will be selectable.

    selectOnClose: boolean = false

    Directly maps to select2 option selectOnClose.

    allowClear: boolean = true

    Whether or not to allow the current select to be set to null. Directly maps to select2 option allowClear.

    placeholder: string = 'select'

    Placeholder when nothing is selected. Directly maps to select2 option placeholder.

    openOnFocus: boolean = false

    If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

    select2

    <select data-bind="select2: selectedNumber">
    +    <option value="1">Option 1</option>
    +    <option value="2">Option 2</option>
    +</select>
    +

    Sets up a basic select2 dropdown on an HTML select element. Dropdown contents should be populated through other means - either using stock Knockoutopen in new window bindings or server-side static contents (via cshtml).

    selectOnClose: boolean = false

    Directly maps to select2 option selectOnClose.

    allowClear: boolean = true

    Whether or not to allow the current select to be set to null. Directly maps to select2 option allowClear.

    placeholder: string = 'select'

    Placeholder when nothing is selected. Directly maps to select2 option placeholder.

    openOnFocus: boolean = false

    If true, the dropdown will open when tabbed to. Browser support may be incomplete in some versions of IE.

    datePicker

    <div class="input-group date">
    +    <input data-bind="datePicker: birthDate" type="text" class="form-control" id-prefix="datePicker" />
    +    <span class="input-group-addon">
    +        <span class="fa fa-calendar"></span>
    +    </span>
    +</div>
    +

    Creates a date/time picker for changing a moment.Moment property. The control used is bootstrap-datetimepickeropen in new window

    preserveDate: boolean = false

    If true, the date portion of the moment.Moment object will be preserved by the date picker. Only the time portion will be changed by user input.

    preserveTime: boolean = false

    If true, the time portion of the moment.Moment object will be preserved by the date picker. Only the date portion will be changed by user input.

    format: string = 'M/D/YY h:mm a'

    Specify the moment-compatible format string to be used as the display format for the text value shown on the date picker. Defaults to M/D/YY h:mm a. Direct pass-through to bootstrap-datetimepickeropen in new window.

    sideBySide: boolean = false

    If true, places the time picker next to the date picker, visible at the same time. Direct pass-through to corresponding bootstrap-datetimepickeropen in new window option.

    stepping: number = 1

    Direct pass-through to corresponding bootstrap-datetimepickeropen in new window option.

    timeZone: string = ''

    Direct pass-through to corresponding bootstrap-datetimepickeropen in new window option.

    keyBinds = { left: null, right: null, delete: null }

    Override key bindings of the date picker. Direct pass-through to corresponding bootstrap-datetimepickeropen in new window option.

    updateImmediate: boolean = false

    If true, the datePicker will update the underlying observable on each input change. Otherwise, the observable will only be changed when the datePicker loses focus (on blur).

    saveImmediately

    <div data-bind="with: product">
    +    <input type="text" data-bind="textValue: description, saveImmediately: true" />
    +</div>
    +

    When used in a context where $data is a Coalesce.BaseViewModel, that object's saveTimeoutMs configuration property (see ViewModel Configuration) will be set to 0 when the element it is placed on gains focus. This value will be reverted to its previous value when the element loses focus. This will cause any changes to the object, including any observable bound as input on the element, to trigger a save immediately rather than after a delay (defaults to 500ms).

    delaySave

    <div data-bind="with: product">
    +    <input type="text" data-bind="textValue: description, delaySave: true" />
    +</div>
    +

    When used in a context where $data is a Coalesce.BaseViewModel, that object's autoSaveEnabled configuration property (see ViewModel Configuration) will be set to false when the element it is placed on gains focus. This will cause any changes to the object, including any observable bound as input on the element, to not trigger auto saves while the element has focus. When the element loses focus, the autoSaveEnabled flag will be reverted to its previous value and an attempt will be made to save the object.

    Display Bindings

    tooltip

    <div data-bind="tooltip: tooltipText">Some Element</div>
    +<div data-bind="tooltip: {title: note, placement: 'bottom', animation: false}">Some Element</div>
    +

    Wrapper around the Bootstrap tooltip componentopen in new window. Binding can either be simply a string (or observable string), or it can be an object that will be passed directly to the Bootstrap tooltip component.

    fadeVisible

    <div data-bind="fadeVisible: isVisible">Some Element</div>
    +

    Similar to the Knockout visible binding, but uses jQuery fadeIn/fadeOut calls to perform the transition.

    slideVisible

    <div data-bind="slideVisible: isVisible">Some Element</div>
    +

    Similar to the Knockout visible, but uses jQuery slideIn/slideOut calls to perform the transition.

    moment

    <div data-bind="moment: momentObservable"></div>
    +<div data-bind="moment: momentObservable, format: 'MM/DD/YYYY hh:mm a'"></div>
    +

    Controls the text of the element by calling the format method on a moment object.

    momentFromNow

    <div data-bind="momentFromNow: momentObservable"></div>
    +<div data-bind="momentFromNow: momentObservable, shorten: true"></div>
    +

    Controls the text of the element by calling the fromNow method on a moment object. If shorten is true, certain phrases will be slightly shortened.

    Utility Bindings

    let

    <div class="item">
    +    <!-- ko let: { showControls: $data.isEditing() || $parent.editingChildren() } -->
    +    <button data-bind="click: $root.editItem, visible: showControls">Edit</button>
    +    <span data-bind="text: name"></span>
    +    <button data-bind="click: $root.deleteItem, visible: showControls">Delete</button>
    +    <!-- /ko -->
    +</div>
    +

    The let binding is a somewhat common construct used in Knockout applications, but isn't part of Knockout itself. It effectively allows the creation of variables in the binding context, allowing complex statements which may be used multiple times to be aliased for both clarity of code and better performance.

    Knockout Binding Defaults

    Knockout Helpers

    These are static properties on IntelliTect.Coalesce.Knockout.Helpers.Knockout you can assign to somewhere in the app lifecycle startup to change the default markup generated server-side when using @Knockout.* methods to render Knockout bindings in your .cshtml files.

    public static int DefaultLabelCols { get; set; } = 3;

    The default number of Bootstrap grid columns a field label should span across.

    public static int DefaultInputCols { get; set; } = 9;

    The default number of Bootstrap grid columns a form input should span across.

    public static string DefaultDateFormat { get; set; } = "M/D/YYYY";

    Sets the default date-only format to be used by all date/time pickers. This only applies to models with a date-only [DateType] attribute.

    public static string DefaultTimeFormat { get; set; } = "h:mm a";

    Sets the default time-only format to be used by all date/time pickers. This only applies to models with a time-only [DateType] attribute.

    public static string DefaultDateTimeFormat { get; set; } = "M/D/YYYY h:mm a";

    Sets the default date/time format to be used by all date/time pickers. This only applies to DateTimeOffset model properties that do not have a limiting [DateType] attribute.

    Note

    DefaultDateFormat, DefaultTimeFormat and DefaultDateTimeFormat all take various formatting strings from the Moment.js library. A full listing can be found on the Moment websiteopen in new window.

    Timezone

    The date/time picker properties can be coupled with DateTimeOffset model properties to display time values localized for the current user's locale. If you want to make the localization static, simply include a script block in your _Layout.cshtml or in a specific view that sets the default for Moment.js:

    <script>
    +moment.tz.setDefault("America/Chicago");
    +</script>
    +

    Note

    This needs to happen after Moment is loaded, but before the bootstrap-datetimepicker script is loaded.

    + + + diff --git a/stacks/ko/client/external-view-model.html b/stacks/ko/client/external-view-model.html new file mode 100644 index 000000000..36d76844e --- /dev/null +++ b/stacks/ko/client/external-view-model.html @@ -0,0 +1,40 @@ + + + + + + + + + TypeScript External ViewModels | Coalesce + + + + +

    TypeScript External ViewModels

    For all External Types in your model, Coalesce will generate a TypeScript class that provides a bare-bones representation of that type's properties.

    These ViewModels are dependent on Knockoutopen in new window, and are designed to be used directly from Knockout bindings in your HTML. All data properties on the generated model are Knockout observables.

    Base Members

    The TypeScript ViewModels for external types do not have a common base class, and do not have any of the behaviors or convenience properties that the regular TypeScript ViewModels for database-mapped classes have.

    Model-Specific Members

    Data Properties

    
    +public personId: KnockoutObservable<number | null> = ko.observable(null);
    +public fullName: KnockoutObservable<string | null> = ko.observable(null);
    +public gender: KnockoutObservable<number | null> = ko.observable(null);
    +public companyId: KnockoutObservable<number | null> = ko.observable(null);
    +public company: KnockoutObservable<ViewModels.Company | null> = ko.observable(null);
    +public addresses: KnockoutObservableArray<ViewModels.Address> = ko.observableArray([]);
    +public birthDate: KnockoutObservable<moment.Moment | null> = ko.observable(moment());

    For each exposed property on the underlying EF POCO, a KnockoutObservable<T> property will exist on the TypeScript model. For navigation properties, these will be typed with the corresponding TypeScript ViewModel for the other end of the relationship. For collections (including collection navigation properties), these properties will be KnockoutObservableArray<T> objects.

    Enum Members

    For each enum property on your POCO, the following will be created:

    public genderText: KnockoutComputed<string | null>

    A KnockoutComputed<string> property that will provide the text to display for that property.

    + + + diff --git a/stacks/ko/client/list-view-model.html b/stacks/ko/client/list-view-model.html new file mode 100644 index 000000000..92b23a1f0 --- /dev/null +++ b/stacks/ko/client/list-view-model.html @@ -0,0 +1,45 @@ + + + + + + + + + TypeScript ListViewModels | Coalesce + + + + +

    TypeScript ListViewModels

    In addition to TypeScript ViewModels for interacting with instances of your data classes in TypeScript, Coalesce will also generated a List ViewModel for loading searched, sorted, paginated data from the server.

    These ListViewModels, like the ViewModels, are dependent on Knockout and are designed to be used directly from Knockout bindings in your HTML.

    Base Members

    The following members are defined on BaseListViewModel<> and are available to the ListViewModels for all of your model types:

    modelKeyName: string

    Name of the primary key of the model that this list represents.

    includes: string

    String that is used to control loading and serialization on the server. See Includes String for more information.

    items: KnockoutObservableArray<TItem>

    The collection of items that have been loaded from the server.

    addNewItem: (): TItem

    Adds a new item to the items collection.

    deleteItem: (item: TItem): JQueryPromise<any>

    Deletes an item and removes it from the items collection.

    queryString: string

    An arbitrary URL query string to append to the API call when loading the list of items.

    Search criteria for the list. This can be easily bound to with a text box for easy search behavior. See [Search] for a detailed look at how searching works in Coalesce.

    isLoading: KnockoutObservable<boolean>

    True if the list is loading.

    isLoaded: KnockoutObservable<boolean>

    True once the list has been loaded.

    load: (callback?: any): JQueryPromise<any>

    Load the list using current parameters for paging, searching, etc Result is placed into the items property.

    message: KnockoutObservable<string>

    If a load failed, this is a message about why it failed.

    getCount: (callback?: any): JQueryPromise<any>

    Gets the count of items without getting all the items. Result is placed into the count property.

    count: KnockoutObservable<number>

    The result of getCount(), or the total on this page.

    totalCount: KnockoutObservable<number>

    Total count of items, even ones that are not on the page.

    nextPage: (): void

    Change to the next page.

    nextPageEnabled: KnockoutComputed<boolean>

    True if there is another page after the current page.

    previousPage: (): void

    Change to the previous page.

    previousPageEnabled: KnockoutComputed<boolean>

    True if there is another page before the current page.

    page: KnockoutObservable<number>

    Page number. This can be set to get a new page.

    pageCount: KnockoutObservable<number>

    Total page count

    pageSize: KnockoutObservable<number>

    Number of items on a page.

    orderBy: KnockoutObservable<string>

    Name of a field by which this list will be loaded in ascending order.

    If set to "none", default sorting behavior, including behavior defined with use of [DefaultOrderBy] in C# POCOs, is suppressed.

    orderByDescending: KnockoutObservable<string>

    Name of a field by which this list will be loaded in descending order.

    orderByToggle: (field: string): void

    Toggles sorting between ascending, descending, and no order on the specified field.

    Model-Specific Members

    Configuration

    static coalesceConfig: Coalesce.ListViewModelConfiguration<PersonList, ViewModels.Person>

    A static configuration object for configuring all instances of the ListViewModel's type is created. See ViewModel Configuration.

    coalesceConfig: Coalesce.ListViewModelConfiguration<PersonList, ViewModels.Person>

    An per-instance configuration object for configuring each specific ListViewModel instance is created. See ViewModel Configuration.

    Filter Object

    public filter: {
    +    personId?: string
    +    firstName?: string
    +    lastName?: string
    +    gender?: string
    +    companyId?: string
    +} = null;

    For each exposed scalar property on the underlying EF POCO, filter will have a corresponding property. If the filter object is set, requests made to the server to retrieve data will be passed all the values in this object via the URL's query string. These parameters will filter the resulting data to only rows where the parameter values match the row's values. For example, if filter.companyId is set to a value, only people from that company will be returned.

    These parameters all allow for freeform string values, allowing the server to implement any kind of filtering logic desired. The Standard Data Source will perform the following depending on the property type:

    • Dates with a time component will be matched exactly.
    • Dates with no time component will match any dates that fell on that day.
    • Strings will match exactly unless an asterisk is found, in which case they will be matched with string.StartsWith.
    • Enums will match by string or numeric value. Multiple comma-delimited values will create a filter that will match on any of the provided values.
    • Numeric values will match exactly. Multiple comma-delimited values will create a filter that will match on any of the provided values.

    Example usage:

    var list = new ListViewModels.PersonList();
    +list.filter = { lastName: "Erickson" };
    +list.load();
    +

    Static Method Members

    public readonly namesStartingWith = new Person.NamesStartingWith(this);
    +public static NamesStartingWith = class NamesStartingWith extends Coalesce.ClientMethod<PersonList, string[]> { ... };

    For each exposed Static Method on your POCO, the members outlined in Methods - Generated TypeScript will be created.

    DataSources

    
    +public dataSources = ListViewModels.PersonDataSources;
    +public dataSource: DataSource<Person> = new this.dataSources.Default();

    For each of the Data Sources on the class, a corresponding class will be added to a namespace named ListViewModels.<ClassName>DataSources. This namespace can always be accessed on both ViewModel and ListViewModel instances via the dataSources property, and class instances can be assigned to the dataSource property.

    + + + diff --git a/stacks/ko/client/methods.html b/stacks/ko/client/methods.html new file mode 100644 index 000000000..e35bdc65a --- /dev/null +++ b/stacks/ko/client/methods.html @@ -0,0 +1,38 @@ + + + + + + + + + TypeScript Method Objects | Coalesce + + + + +

    TypeScript Method Objects

    For each Custom Method you define, a class will be created on the corresponding TypeScript ViewModel (instance methods) or ListViewModel (static methods) that contains the properties and functions for interaction with the method. This class is accessible through a static property named after the method. An instance of this class will also be created on each instance of its parent - this instance is in a property with the camel-cased name of the method.

    Here's an example for a method called Rename that takes a single parameter 'string name' and returns a string.

    public string Rename(string name)
    +{
    +    FirstName = name;
    +    return FullName; // Return the new full name of the person.
    +}
    +

    Base Members

    The following members are available on the method object for all client methods:

    public result: KnockoutObservable<string>

    Observable that will contain the results of the method call after it is complete.

    public rawResult: KnockoutObservable<Coalesce.ApiResult>

    Observable with the raw, deserialized JSON result of the method call. If the method call returns an object, this will contain the deserialized JSON object from the server before it has been loaded into ViewModels and its properties loaded into observables.

    public isLoading: KnockoutObservable<boolean>

    Observable boolean which is true while the call to the server is pending.

    public message: KnockoutObservable<string>

    If the method was not successful, this contains exception information.

    public wasSuccessful: KnockoutObservable<boolean>

    Observable boolean that indicates whether the method call was successful or not.

    ListResult<T> Base Members

    For methods that return a ListResult<T>, the following additional members on the method object will be available:

    public page: KnockoutObservable<number>

    Page number of the results.

    public pageSize: KnockoutObservable<number>

    Page size of the results.

    public pageCount: KnockoutObservable<number>

    Total number of possible result pages.

    public totalCount: KnockoutObservable<number>

    Total number of results.

    Method-specific Members

    public static Rename = class Rename extends Coalesce.ClientMethod<Person, string> { ... }

    Declaration of the method object class. This will be generated on the parent ViewModel or ListViewModel.

    public readonly rename = new Person.Rename(this)

    Default instance of the method for easy calling of the method without needing to manually instantiate the class. This will be generated on the parent ViewModel or ListViewModel.

    public invoke: (name: string, callback: (result: string) => void = null, reload: boolean = true): JQueryPromise<any>

    Function that takes all the method parameters and a callback. If reload is true, the ViewModel or ListViewModel that owns the method will be reloaded after the call is complete, and only after that happens will the callback be called.

    public static Args = class Args { public name: KnockoutObservable<string> = ko.observable(null); }

    Class with one observable member per method argument for binding method arguments to user input. Only generated for methods with arguments.

    public args = new Rename.Args()

    Default instance of the args class. Only generated for methods with arguments.

    public invokeWithArgs: (args = this.args, callback?: (result: string) => void, reload: boolean = true) => JQueryPromise<any>

    Function for invoking the method using the args class. The default instance of the args class will be used if none is provided. Only generated for methods with arguments.

    public invokeWithPrompts: (callback: (result: string) => void = null, reload: boolean = true) => JQueryPromise<any>

    Simple interface using browser prompt() input boxes to prompt the user for the required data for the method call. The call is then made with the data provided. Only generated for methods with arguments.

    public resultObjectUrl: KnockoutObservable<string | null>

    Observable that will contain an Object URLopen in new window representing the last successful invocation result. Only generated for methods that return a file.

    public url: KnockoutComputed<string>

    The URL for the method. Can be useful for using as the src attribute of an image or video HTML element for file-downloading methods. Any arguments will be populated from this.args. Only generated for HTTP GET methods, as configured by [ControllerAction].

    + + + diff --git a/stacks/ko/client/model-config.html b/stacks/ko/client/model-config.html new file mode 100644 index 000000000..d2f2b506c --- /dev/null +++ b/stacks/ko/client/model-config.html @@ -0,0 +1,40 @@ + + + + + + + + + ViewModel Configuration | Coalesce + + + + +

    ViewModel Configuration

    A crucial part of the generated TypeScript ViewModels that Coalesce creates for you is the hierarchical configuration system that allows coarse-grained or fine-grained control over their behaviors.

    Hierarchy

    The configuration system has four levels where configuration can be performed, structured as follows:

    Root Configuration

    Coalesce.GlobalConfiguration: ModelConfiguration<any>
    +Coalesce.GlobalConfiguration.app: AppConfiguration

    The root configuration contains all configuration properties which apply to class category (TypeScript ViewModels, TypeScript ListViewModels, and Services). The app property contains global app configuration that exists independent of any models. Then, for each class kind, the following are available:

    Root ViewModel/ListViewModel Configuration

    Coalesce.GlobalConfiguration.viewModel: ViewModelConfiguration<BaseViewModel>
    +Coalesce.GlobalConfiguration.listViewModel: ListViewModelConfiguration<BaseListViewModel<BaseViewModel>, BaseViewModel>
    +Coalesce.GlobalConfiguration.serviceClient: ServiceClientConfiguration<ServiceClient>

    Additional root configuration objects exist, one for each class kind. These configuration objects govern behavior that applies to only objects of these types. Root configuration can be overridden using these objects, although the practicality of doing so is dubious.

    Class Configuration

    ViewModels.ClassName.coalesceConfig: ViewModelConfiguration<ViewModels.ClassName>
    +ListViewModels.ClassNameList.coalesceConfig: ListViewModelConfiguration<ListViewModels.ClassNameList, ViewModels.ClassName>
    +Services.ServiceNameClient.coalesceConfig: ServiceClientConfiguration<ServiceName>

    Each class kind has a static property named coalesceConfig that controls behavior for all instances of that class.

    Instance Configuration

    instance.coalesceConfig: ViewModelConfiguration<ViewModels.ClassName>
    +listInstance.coalesceConfig: ListViewModelConfiguration<ListViewModels.ClassNameList, ViewModels.ClassName>
    +serviceInstance.coalesceConfig: ServiceClientConfiguration<ServiceName>

    Each instance of these classes also has a coalesceConfig property that controls behaviors for that instance only.

    Evaluation

    All configuration properties are Knockout ComputedObservable<T> objects. These observables behave like any other observable - call them with no parameter to obtain the value, call with a parameter to set their value.

    Whenever a configuration property is read from, it first checks its own configuration object for the value of that property. If the explicit value for that configuration object is null, the parent's configuration will be checked for a value. This continues until either a value is found or the root configuration object is reached.

    When a configuration property is given a value, that value is established on that configuration object only. Any dependent configuration objects will not be modified, and if those dependent configuration objects already have a value for that property, their existing value will be used unless that value is later set to null.

    To obtain the raw value for a specific configuration property, call the raw() method on the observable: model.coalesceConfig.autoSaveEnabled.raw().

    Available Properties & Defaults

    The following configuration properties are available. Their default values are also listed. Note that all configuration properties are observables, but for simplicity the documentation below lists the underlying type.

    Root Configuration

    These properties on Coalesce.GlobalConfiguration are available to both ViewModelConfiguration, ListViewModelConfiguration, and ServiceClientConfiguration.

    baseApiUrl: string = '/api'

    The relative url where the API may be found.

    baseViewUrl: string = ''

    The relative url where the admin views may be found.

    showFailureAlerts: boolean = true

    Whether or not the callback specified for onFailure will be called or not.

    onFailure: (obj, message) => alert(message)

    A callback to be called when a failure response is received from the server.

    onStartBusy: obj => Coalesce.Utilities.showBusy()

    A callback to be called when an AJAX request begins.

    onFinishBusy: obj => Coalesce.Utilities.hideBusy()

    A callback to be called when an AJAX request completes.

    App Configuration

    These properties on Coalesce.GlobalConfiguration.app are not hierarchical - they govern the entire Coalesce application:

    select2Theme: string | null = null

    The theme parameter to select2's constructor when called by Coalesce's select2 Knockout Bindings.

    ViewModelConfiguration

    saveTimeoutMs: number = 500

    Time to wait after a change is seen before auto-saving (if autoSaveEnabled is true). Acts as a debouncing timer for multiple simultaneous changes.

    saveIncludedFields: string[] | null = null

    An array of property names that, if set, will determine which fields will be sent to the server when saving. Only those values that are actually sent to the server will be mapped to the underlying entity.

    This can improves the handling of concurrent changes being made by multiple users against different fields of the same entity. Specifically, if one page is designed to edit fields A and B, and another page is designed for editing fields C and D, you can configure this setting appropriately on each page to only save the corresponding fields.

    Due to design limitations, this cannot be determined dynamically like it can with Vue's $saveMode property

    WARNING

    Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the x-www-form-urlencoded body that is sent to the server.

    The Generated C# DTOs implement the necessary logic for this; however, any Custom DTOs must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the Generated C# DTOs, or do not use surgical saves with Custom DTOs.

    autoSaveEnabled: boolean = true

    Determines whether changes to a model will be automatically saved after saveTimeoutMs milliseconds have elapsed.

    autoSaveCollectionsEnabled: boolean = true

    Determines whether or not changes to many-to-many collection properties will automatically trigger a save call to the server or not.

    showBusyWhenSaving: boolean = false

    Whether to invoke onStartBusy and onFinishBusy during saves.

    loadResponseFromSaves: boolean = true

    Whether or not to reload the ViewModel with the state of the object received from the server after a call to .save().

    validateOnLoadFromDto: boolean = true

    Whether or not to validate the model after loading it from a DTO from the server. Disabling this can improve performance in some cases.

    setupValidationAutomatically: boolean = true

    Whether or not validation on a ViewModel should be setup in its constructor, or if validation must be set up manually by calling viewModel.setupValidation(). Turning this off can improve performance in read-only scenarios.

    onLoadFromDto: null | ((object: T) => void) = null

    An optional callback to be called when an object is loaded from a response from the server. Callback will be called after all properties on the ViewModel have been set from the server response.

    initialDataSource: null | DataSource<T> | (new () => DataSource<T>) = null

    The dataSource (either an instance or a type) that will be used as the initial dataSource when a new object of this type is created. Not valid for global configuration; recommended to be used on class-level configuration. E.g. ViewModels.MyModel.coalesceConfig.initialDataSource(MyModel.dataSources.MyDataSource);

    ListViewModelConfiguration

    No special configuration is currently available for ListViewModels.

    ServiceClientConfiguration

    No special configuration is currently available for ServiceClients.

    + + + diff --git a/stacks/ko/client/view-model.html b/stacks/ko/client/view-model.html new file mode 100644 index 000000000..00d517146 --- /dev/null +++ b/stacks/ko/client/view-model.html @@ -0,0 +1,54 @@ + + + + + + + + + TypeScript ViewModels | Coalesce + + + + +

    TypeScript ViewModels

    For each database-mapped type in your model, Coalesce will generate a TypeScript class that provides a multitude of functionality for interacting with the data on the client.

    These ViewModels are dependent on Knockoutopen in new window, and are designed to be used directly from Knockout bindings in your HTML. All data properties on the generated model are Knockout observables.

    Base Members

    The following base members are available to all generated ViewModel classes:

    includes: string

    String that will be passed to the server when loading and saving that allows for data trimming via C# Attributes. See Includes String.

    isChecked: KnockoutObservable<boolean>

    Flag to use to determine if this item is checked. Only provided for convenience.

    isSelected: KnockoutObservable<boolean>

    Flag to use to determine if this item is selected. Only provided for convenience.

    isEditing: KnockoutObservable<boolean>

    Flag to use to determine if this item is being edited. Only provided for convenience.

    toggleIsEditing () => void

    Toggles the isEditing flag.

    isExpanded: KnockoutObservable<boolean>

    Flag to use to determine if this item is expanded. Only provided for convenience.

    toggleIsExpanded: () => void

    Toggles the isExpanded flag.

    isVisible: KnockoutObservable<boolean>

    Flag to use to determine if this item is shown. Only provided for convenience.

    toggleIsSelected () => void

    Toggles the isSelected flag.

    selectSingle: (): boolean

    Sets isSelected(true) on this object and clears on the rest of the items in the parent collection.

    isDirty: KnockoutObservable<boolean>

    Dirty Flag. Set when a value on the model changes. Reset when the model is saved or reloaded.

    isLoaded: KnockoutObservable<boolean>

    True once the data has been loaded.

    isLoading: KnockoutObservable<boolean>

    True if the object is loading.

    isSaving: KnockoutObservable<boolean>

    True if the object is currently saving.

    isThisOrChildSaving: KnockoutComputed<boolean>

    Returns true if the current object, or any of its children, are saving.

    load: id: any, callback?: (self: T) => void): JQueryPromise<any> | undefined

    Loads the object from the server based on the id specified. If no id is specified, the current id, is used if one is set.

    loadChildren: callback?: () => void) => void

    Loads any child objects that have an ID set, but not the full object. This is useful when creating an object that has a parent object and the ID is set on the new child.

    loadFromDto: data: any, force?: boolean, allowCollectionDeletes?: boolean) => void

    Loads this object from a data transfer object received from the server.

    • force - Will override the check against isLoading that is done to prevent recursion.
    • allowCollectionDeletes - Set true when entire collections are loaded. True is the default. In some cases only a partial collection is returned, set to false to only add/update collections.

    deleteItem: callback?: (self: T) => void): JQueryPromise<any> | undefined

    Deletes the object without any prompt for confirmation.

    deleteItemWithConfirmation: callback?: () => void, message?: string): JQueryPromise<any> | undefined

    Deletes the object if a prompt for confirmation is answered affirmatively.

    errorMessage: KnockoutObservable<string>

    Contains the error message from the last failed call to the server.

    onSave: callback: (self: T) => void): boolean

    Register a callback to be called when a save is done. Returns true if the callback was registered, or false if the callback was already registered.

    saveToDto: () => any

    Saves this object into a data transfer object to send to the server.

    save: callback?: (self: T) => void): JQueryPromise<any> | boolean | undefined

    Saves the object to the server and then calls a callback. Returns false if there are validation errors.

    parent: any

    Parent of this object, if this object was loaded as part of a hierarchy.

    parentCollection: KnockoutObservableArray<T>

    Parent of this object, if this object was loaded as part of list of objects.

    editUrl: KnockoutComputed<string>

    URL to a stock editor for this object.

    showEditor: callback?: any): JQueryPromise<any>

    Displays an editor for the object in a modal dialog.

    validate: (): boolean

    Triggers any validation messages to be shown, and returns a bool that indicates if there are any validation errors.

    validationIssues: any

    ValidationIssues returned from the server when trying to persist data

    warnings: KnockoutValidationErrors

    List of warnings found during validation. Saving is still allowed with warnings present.

    errors: KnockoutValidationErrors

    List of errors found during validation. Any errors present will prevent saving.

    Model-Specific Members

    The following members are generated for each generated ViewModel class and are unique to each class. The examples below are based on a type named Person.

    Configuration

    static coalesceConfig: Coalesce.ViewModelConfiguration<Person>

    A static configuration object for configuring all instances of the ViewModel's type is created. See ViewModel Configuration.

    coalesceConfig: Coalesce.ViewModelConfiguration<Person>

    An per-instance configuration object for configuring each specific ViewModel instance is created. See ViewModel Configuration.

    DataSources

    
    +public dataSources = ListViewModels.PersonDataSources;
    +public dataSource: DataSource<Person> = new this.dataSources.Default();

    For each of the Data Sources for a model, a class will be added to a namespace named ListViewModels.<ClassName>DataSources. This namespace can always be accessed on both ViewModel and ListViewModel instances via the dataSources property, and class instances can be assigned to the dataSource property.

    Data Properties

    
    +public personId: KnockoutObservable<number | null> = ko.observable(null);
    +public fullName: KnockoutObservable<string | null> = ko.observable(null);
    +public gender: KnockoutObservable<number | null> = ko.observable(null);
    +public companyId: KnockoutObservable<number | null> = ko.observable(null);
    +public company: KnockoutObservable<ViewModels.Company | null> = ko.observable(null);
    +public addresses: KnockoutObservableArray<ViewModels.Address> = ko.observableArray([]);
    +public birthDate: KnockoutObservable<moment.Moment | null> = ko.observable(moment());

    For each exposed property on the underlying EF POCO, a KnockoutObservable<T> property will exist on the TypeScript model. For navigation properties, these will be typed with the corresponding TypeScript ViewModel for the other end of the relationship. For collections (including collection navigation properties), these properties will be KnockoutObservableArray<T> objects.

    Enum Members

    For each enum property on your POCO, the following will be created:

    public genderText: KnockoutComputed<string | null>

    A KnockoutComputed<string> property that will provide the text to display for that property.

    public genderValues: Coalesce.EnumValue[] = [ 
    +    { id: 1, value: 'Male' },
    +    { id: 2, value: 'Female' },
    +    { id: 3, value: 'Other' },
    +];

    A static array of objects with properties id and value that represent all the values of the enum.

    
    +export namespace Person {
    +    export enum GenderEnum {
    +        Male = 1,
    +        Female = 2,
    +        Other = 3,
    +    };
    +}

    A TypeScript enum that mirrors the C# enum directly. This enum is in a sub-namespace of ViewModels named the same as the class name.

    Collection Navigation Property Helpers

    For each collection navigation property on the POCO, the following members will be created:

    public addToAddresses: (autoSave?: boolean) => ViewModels.Address;

    A method that will add a new object to that collection property. If autoSave is specified, the auto-save behavior of the new object will be set to that value. Otherwise, the inherited default will be used (see ViewModel Configuration)

    public addressesListUrl: KnockoutComputed<string>;

    A KnockoutComputed<string> that evaluates to a relative url for the generated table view that contains only the items that belong to the collection navigation property.

    Reference Navigation Property Helpers

    public showCompanyEditor: (callback?: any) => void;

    For each reference navigation property on the POCO a method will be created that will call showEditor on that current value of the navigation property, or on a new instance if the current value is null.

    public companyText: KnockoutComputed<string>;

    For each reference navigation property, a KnockoutComputed<string> property will be created that will provide the text to display for that property. This will be the property on the class annotated with [ListText].

    Instance Method Members

    public readonly getBirthDate = new Person.GetBirthDate(this);
    +public static GetBirthDate = class GetBirthDate extends Coalesce.ClientMethod<Person, moment.Moment> { ... };

    For each Instance Method on your POCO, a class and instance member will be created as described in Methods - Generated TypeScript.

    + + + diff --git a/stacks/ko/getting-started.html b/stacks/ko/getting-started.html new file mode 100644 index 000000000..3225965d0 --- /dev/null +++ b/stacks/ko/getting-started.html @@ -0,0 +1,88 @@ + + + + + + + + + Getting Started with Knockout | Coalesce + + + + +

    Getting Started with Knockout

    Creating a Project

    WARNING

    The Coalesce Knockout.js stack is deprecated and will be receiving only critical bug fixes going forward. You are strongly encouraged to start all new Coalesce projects with Vue.

    The quickest and easiest way to create a new Coalesce Knockout application is to use the dotnet new template. In your favorite shell:

    dotnet new install IntelliTect.Coalesce.KnockoutJS.Template
    +dotnet new coalesceko
    +

    open in new window • View on GitHubopen in new window

    Data Modeling

    At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:

    • Create an initial Data Model by adding EF entity classes to the data project and the corresponding DbSet<> properties to AppDbContext. You will notice that the starter project includes a single model, ApplicationUser, to start with. Feel free to change this model or remove it entirely. Read Entity Models for more information about creating a data model.

    • Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.

    • Run Coalesce's code generation by either:

      • Running dotnet coalesce in the web project's root directory
      • Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer

    You're now at a point where you can start creating your own pages!

    Building Pages & Features

    Lets say we've created a model called Person as follows, and we've ran code generation with dotnet coalesce:

    namespace MyApplication.Data.Models 
    +{
    +    public class Person
    +    {
    +        public int PersonId { get; set; }
    +        public string Name { get; set; }
    +        public DateTimeOffset? BirthDate { get; set; }
    +    }
    +}
    +

    We can create a details page for a Person by creating:

    • A controller in src/MyApplication.Web/Controllers/PersonController.cs:

      namespace MyApplication.Web.Controllers
      +{
      +    public partial class PersonController
      +    {
      +        public IActionResult Details() => View();
      +    }
      +}
      +
    • A view in src/MyApplication.Web/Views/Person/Details.cshtml:

      <h1>Person Details</h1>
      +
      +<div data-bind="with: person">
      +    <dl class="dl-horizontal">
      +        <dt>Name </dt>
      +        <dd data-bind="text: name"></dd>
      +
      +        <dt>Date of Birth </dt>
      +        <dd data-bind="moment: birthDate, format: 'MM/DD/YYYY hh:mm a'"></dd>
      +    </dl>
      +</div>
      +
      +@section Scripts
      +{
      +<script src="~/js/person.details.js"></script>
      +<script>
      +    $(function () {
      +        var vm = new MyApplication.PersonDetails();
      +        ko.applyBindings(vm);
      +        vm.load();
      +    });
      +</script>
      +}
      +
    • And a script in src/MyApplication.Web/Scripts/person.details.ts:

      /// <reference path="viewmodels.generated.d.ts" />
      +
      +module MyApplication {
      +    export class PersonDetails {
      +        public person = new ViewModels.Person();
      +
      +        load() {
      +            var id = Coalesce.Utilities.GetUrlParameter("id");
      +            if (id != null && id != '') {
      +                this.person.load(id);
      +            }
      +        }
      +    }
      +}
      +

    With these pieces in place, we now have a functioning page that will display details about a person. We can start up the application and navigate to /Person/Details?id=1 (assuming a person with ID 1 exists - if not, navigate to /Person/Table and create one).

    From this point, one can start adding more fields, more features, and more flair to the page. Check out all the other documentation in the sidebar to see what else Coalesce has to offer, including the Knockout Overview.

    + + + diff --git a/stacks/ko/overview.html b/stacks/ko/overview.html new file mode 100644 index 000000000..ef02d0529 --- /dev/null +++ b/stacks/ko/overview.html @@ -0,0 +1,33 @@ + + + + + + + + + Knockout Overview | Coalesce + + + + +

    Knockout Overview

    The Knockoutopen in new window stack for Coalesce offers the ability to build pages with the time-tested Knockoutopen in new window JavaScript library using all of the features of the Coalesce generated APIs and ViewModels. It can be used for anything between adding simple interactive augmentations of MVC pages to building a full MPA-SPA hybrid application.

    Getting Started

    Check out Getting Started with Knockout if you haven't already to learn how to get a new Coalesce Knockout project up and running.

    Generated Code

    Below you will find a brief overview of each of the different pieces of code that Coalesce will generate for you when you choose the Knockout stack.

    TypeScript

    Coalesce generates a number of different types of TypeScript classes to support your data through the generated API.

    ViewModels

    One view model class is generated for each of your Entity Models and Custom DTOs. These models contain fields for your model Properties, and functions and other members for your model Methods. They also contain a number of standard fields & functions inherited from BaseViewModel which offer basic loading & saving functionality, as well as other handy utility members for use with Knockout.

    See TypeScript ViewModels for more details.

    List ViewModels

    One ListViewModel is generated for each of your Entity Models and Custom DTOs. These classes contain functionality for loading sets of objects from the server. They provide searching, paging, sorting, and filtering functionality.

    See TypeScript ListViewModels for more details.

    External Type ViewModels

    Any non-primitive types which are not themselves a Entity Models or Custom DTOs which are accessible through the aforementioned types, either through one of its Properties, or return value from one of its Methods, will have a corresponding TypeScript ViewModel generated for it. These ViewModels only provide a KnockoutObservable field for each property on the C# class.

    See TypeScript External ViewModels for more details.

    View Controllers

    For each of your Entity Models and Custom DTOs, a controller is created in the /Controllers/Generated directory of your web project. These controllers provide routes for the generated admin views.

    As you add your own pages to your application, you should add additional partial classes in the /Controllers that extend these generated partial classes to expose those pages.

    Admin Views

    For each of your Entity Models and Custom DTOs, a number of views are generated to provide administrative-level access to your data.

    Table

    Provides a basic table view with sorting, searching, and paging of your data. Can be rendered in either read-only mode (routed as /Table), or editable mode (routed as TableEdit).

    Cards

    Provides a card-based view of your data with searching and paging.

    CreateEdit

    Provides an editor view which can be used to create new entities or edit existing ones.

    EditorHtml

    Provides a minimal amount of HTML to display an editor for the object type. This is used by the showEditor method on the generated TypeScript ViewModels.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html new file mode 100644 index 000000000..7173290f9 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-display.html @@ -0,0 +1,41 @@ + + + + + + + + + c-admin-display | Coalesce + + + + +

    c-admin-display

    Behaves the same as c-display, except any collection navigation properties will be rendered as links to an admin list page, and any models will be rendered as a link to an admin item page.

    Links for collections are resolved from vue-routeropen in new window with a route name of coalesce-admin-list, a type route param containing the name of the collection's type, and a query parameter filter.<foreign key name> with a value of the primary key of the owner of the collection. This route is expected to resolve to a c-admin-table-page, which is setup by default by the template outlined in Getting Started with Vue.

    Links for single models are resolved from vue-routeropen in new window with a route name of coalesce-admin-item, a type route param containing the name of the model's type, and a id route param containing the object's primary key. This route is expected to resolve to a c-admin-editor-page, which is setup by default by the template outlined in Getting Started with Vue.

    Examples

    <!-- Renders regularly as text: -->
    +<c-admin-display :model="person" for="firstName" />
    +
    +<!-- Renders as a link to an item: -->
    +<c-admin-display :model="person" for="company" />
    +
    +<!-- Renders as a link to a list: -->
    +<c-admin-display :model="person" for="casesAssigned" />
    +

    Props

    Same as c-display.

    Slots

    Same as c-display.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html new file mode 100644 index 000000000..5a51a6f5d --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor-page.html @@ -0,0 +1,52 @@ + + + + + + + + + c-admin-editor-page | Coalesce + + + + +

    c-admin-editor-page

    A page for a creating/editing single ViewModel instance. Provides a c-admin-editor and a c-admin-methods for the instance. Designed to be routed to directly with vue-routeropen in new window.

    Examples

    // router.ts or main.ts
    +
    +// WITHOUT Vuetify A la carte:
    +import { CAdminEditorPage } from 'coalesce-vue-vuetify';
    +// WITH Vuetify A-la-carte:
    +import { CAdminEditorPage } from 'coalesce-vue-vuetify/lib';
    +
    +const router = new Router({
    +    // ...
    +    routes: [
    +        // ... other routes
    +        {
    +            path: '/admin/:type/edit/:id?',
    +            name: 'coalesce-admin-item',
    +            component: CAdminEditorPage,
    +            props: true,
    +        },
    +    ]
    +})
    +

    Props

    type: string

    The PascalCase name of the type to be created/edited.

    id?: number | string

    The primary key of the item being edited. If null or not provided, the page will be creating a new instance of the provided type instead.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html new file mode 100644 index 000000000..19ec41c61 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-editor.html @@ -0,0 +1,34 @@ + + + + + + + + + c-admin-editor | Coalesce + + + + +

    c-admin-editor

    An editor for a single ViewModel instance. Provides a c-input for each property of the model.

    Does not automatically enable auto-save - if desired, this must be enabled by the implementor of this component.

    Examples

    <c-admin-editor :model="person" />
    +

    Props

    model: ViewModel | ListViewModel

    The ViewModel to render an editor for.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html new file mode 100644 index 000000000..db839f990 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-method.html @@ -0,0 +1,34 @@ + + + + + + + + + c-admin-method | Coalesce + + + + +

    c-admin-method

    Provides an interface for invoking a method and rendering its result, designed to be use in an admin page.

    For each parameter of a method, a c-input will be rendered to accept the input of that parameter. A button is provided to trigger an invocation of the method, progress and errors are rendered with a c-loader-status, and results are rendered with c-display.

    Examples

    <c-admin-method :model="person" for="setTitle" auto-reload-model />
    +

    Props

    for: string | Method

    A metadata specifier for the method. One of:

    • A string with the name of the method belonging to model.
    • A direct reference to a method's metadata object.
    • A string in dot-notation that starts with a type name and ending with a method name.

    model: ViewModel | ListViewModel

    An ViewModel or ListViewModel owning the method and API Caller that was specified by the for prop.

    autoReloadModel?: boolean = false

    True if the model should have its $load invoked after a successful invocation of the method.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html new file mode 100644 index 000000000..4a6721ab2 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-methods.html @@ -0,0 +1,36 @@ + + + + + + + + + c-admin-methods | Coalesce + + + + +

    c-admin-methods

    Renders in a Vuetifyopen in new window v-expansion-panelsopen in new window a c-admin-method for each method on a ViewModel or ListViewModel.

    Examples

    <c-admin-methods :model="person" class="x" auto-reload-model />
    +
    <c-admin-methods :model="person" auto-reload-model />
    +
    <c-admin-methods :model="personList" auto-reload-model />
    +

    Props

    model: ViewModel | ListViewModel

    An ViewModel or ListViewModel whose methods should each render as a c-admin-method.

    autoReloadModel?: boolean = false

    True if the model should have its $load invoked after a successful invocation of any method.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html new file mode 100644 index 000000000..86a07f5b8 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-page.html @@ -0,0 +1,52 @@ + + + + + + + + + c-admin-table-page | Coalesce + + + + +

    c-admin-table-page

    A full-featured page for interacting with a ListViewModel. Provides a c-admin-table and a c-admin-methods for the list. Designed to be routed to directly with vue-routeropen in new window.

    Examples

    // router.ts or main.ts
    +
    +// WITHOUT Vuetify A la carte:
    +import { CAdminTablePage } from 'coalesce-vue-vuetify';
    +// WITH Vuetify A-la-carte:
    +import { CAdminTablePage } from 'coalesce-vue-vuetify/lib';
    +
    +const router = new Router({
    +    // ...
    +    routes: [
    +        // ... other routes
    +        {
    +            path: '/admin/:type',
    +            name: 'coalesce-admin-list',
    +            component: CAdminTablePage,
    +            props: true,
    +        },
    +    ]
    +})
    +

    Props

    type: string

    The PascalCase name of the type to be listed.

    list?: ListViewModel

    An optional ListViewModel that will be used if provided instead of the one the component will otherwise create automatically.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html new file mode 100644 index 000000000..e7542e94a --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-table-toolbar.html @@ -0,0 +1,35 @@ + + + + + + + + + c-admin-table-toolbar | Coalesce + + + + +

    c-admin-table-toolbar

    A full-featured toolbar for a ListViewModel designed to be used on an admin page, including "Create" and "Reload" buttons, a c-list-range-display, a c-list-page, a search field, c-list-filters, and a c-list-page-size.

    Examples

    <c-admin-table-toolbar :list="personList" />
    +
    <c-admin-table-toolbar :list="personList" color="pink" :editable.sync="isEditable" />
    +

    Props

    list: ListViewModel

    The ListViewModel to render the toolbar for.

    color: string = 'primary'

    The coloropen in new window of the toolbar.

    editable?: boolean

    If provided, adds a button to toggle the value of this prop. Should be bound with the .sync modifier.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html b/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html new file mode 100644 index 000000000..a87871753 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-admin-table.html @@ -0,0 +1,34 @@ + + + + + + + + + c-admin-table | Coalesce + + + + +

    c-admin-table

    An full-featured table for a ListViewModel, including a c-admin-table-toolbar, c-table, and c-list-pagination.

    The table can be in read mode (default), or toggled into edit mode with the button provided by the c-admin-table-toolbar. When placed into edit mode, auto-save is enabled.

    Examples

    <c-admin-table :list="personList" />
    +

    Props

    list: ListViewModel

    The ListViewModel to render a table for.

    pageSizes?: number[]

    An optional list of available page sizes to offer through the c-list-pagination's c-list-page-size component. Defaults to [10, 25, 100].

    queryBind?: boolean

    If true, the Data Source Standard Parameters of the provided ListViewModel will be read from and written to the window's query string. The "Editable" state of the table will also be bound to the query string.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html b/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html new file mode 100644 index 000000000..1e1643f02 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-datetime-picker.html @@ -0,0 +1,43 @@ + + + + + + + + + c-datetime-picker | Coalesce + + + + +

    c-datetime-picker

    A general, all-purpose date/time input component that can be used either with models and metadata or as a standalone component using only v-model.

    Examples

    <c-datetime-picker :model="person" for="birthDate" />
    +
    +<c-datetime-picker v-model="standaloneDate" />
    +
    +<c-datetime-picker 
    +    v-model="standaloneTime" 
    +    date-kind="time"
    +    date-format="h:mm a"
    +/>
    +

    Props

    for?: string | DateProperty | DateValue

    A metadata specifier for the value being bound. One of:

    • A string with the name of the value belonging to model.
    • A direct reference to a metadata object.
    • A string in dot-notation that starts with a type name.

    model?: Model | DataSource

    An object owning the value that was specified by the for prop. If provided, the input will be bound to the corresponding property on the model object.

    value?: Date // Vue 2
    +modelValue?: Date // Vue 3

    If binding the component with v-model, accepts the value part of v-model.

    dateKind?: 'date' | 'time' | 'datetime' = 'datetime'

    Whether the date is only a date, only a time, or contains significant date and time information.

    If the component was bound with metadata using the for prop, this will default to the kind specified by [DateType].

    dateFormat?: string

    The format of the date that will be rendered in the component's text field, and the format that will be attempted first when parsing user input in the text field.

    Defaults to:

    • M/d/yyyy h:mm a if dateKind == 'datetime',
    • M/d/yyyy if dateKind == 'date', or
    • h:mm a if dateKind == 'time'.

    WARNING

    When parsing a user's text input into the text field, c-datetime-picker will first attempt to parse it with the format specified by dateFormat, or the default as described above if not explicitly specified.

    If this fails, the date will be parsed with the Date constructoropen in new window, but only if the dateKind is datetime or date. This works fairly well on all modern browsers, but can still occasionally have issues. c-datetime-picker tries its best to filter out bad parses from the Date constructor, like dates with a year earlier than 1000.

    native?: boolean

    True if a native HTML5 input should be used instead of a popup menu with Vuetify date/time pickers inside of it.

    sideBySide?: boolean

    True if the calendar and clock should be shown side by side in the picker menu, rather than in separate tabs.

    readonly?: boolean

    True if the component should be read-only.

    disabled?: boolean

    True if the component should be disabled.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-display.html b/stacks/vue/coalesce-vue-vuetify/components/c-display.html new file mode 100644 index 000000000..b9af2d165 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-display.html @@ -0,0 +1,42 @@ + + + + + + + + + c-display | Coalesce + + + + +

    c-display

    A general-purpose component for displaying any Value by rendering the value to a string with the display functions from the Models Layer. For plain string and number values, usage of this component is largely superfluous. For all other value types including dates, booleans, enums, objects, and collections, it is very handy.

    Examples

    Typical usage, providing an object and a property on that object:

    <c-display :model="person" for="gender" />
    +

    Customizing date formatting (view format patternsopen in new window):

    <c-display :model="person" for="birthDate" format="M/d/yyyy" />
    +

    A contrived example of using c-display to render the result of an API Caller:

    <c-display 
    +    :value="person.setFirstName.result" 
    +    :for="person.$metadata.methods.setFirstName.return" 
    +    element="div"
    +/>
    +

    Displaying a standalone date value without a model or other source of metadata:

    <c-display :value="dateProp" format="M/d/yyyy" />
    +

    Props

    for: string | Property | Value

    A metadata specifier for the value being bound. Either a direct reference to the metadata object, or a string with the name of the value belonging to model, or a string in dot-notation that starts with a type name.

    model?: Model | DataSource

    An object owning the value that was specified by the for prop.

    format: DisplayOptions["format"]

    Shorthand for :options="{ format: format }", allowing for specification of the format to be used when displaying dates.

    See DisplayOptions for details on the options available for format.

    options: DisplayOptions

    Specify options for formatting some kinds of values, including dates. See DisplayOptions for details.

    value: any // Vue 2
    +modelValue: any // Vue 3

    Can be provided the value to be displayed in conjunction with the for prop, as an alternative to the model prop.

    This is an uncommon scenario - it is generally easier to use the for/model props together.

    Slots

    default - Used to display fallback content if the value being displayed is either null or "" (empty string).

    [DataTypeAttribute]

    For properties and other values annotated with [DataTypeAttribute], the following special handling occurs based on the data type:

    • DataType.MultilineText: Renders with white-space: pre-wrap.
    • DataType.Password: Renders with a show/hide toggle (hidden by default), showing a fixed number of dot characters when hidden.
    • DataType.Url: Renders as a clickable link.
    • DataType.EmailAddress: Renders as a clickable mailto link.
    • DataType.PhoneNumber: Renders as a clickable tel link.
    • DataType.ImageUrl: Renders as an img element.
    • "Color": Renders a colored dot next to the value, interpreting the field value as a 7-character HTML hex color code..
    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-input.html b/stacks/vue/coalesce-vue-vuetify/components/c-input.html new file mode 100644 index 000000000..2a0dacad0 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-input.html @@ -0,0 +1,44 @@ + + + + + + + + + c-input | Coalesce + + + + +

    c-input

    A general-purpose input component for most Values. c-input does not have much functionality of its own - instead, it delegates to the right kind of component based on the type of value to which it is bound. This includes both other Coalesce Vuetify Components as well as direct usages of some Vuetifyopen in new window components.

    All attributes are passed through to the delegated-to component, allowing for full customization of the underlying Vuetifyopen in new window component.

    A summary of the components delegated to, by type:

    Any other unsupported type will simply be displayed with c-display, unless a default slotopen in new window is provided - in that case, the default slot will be rendered instead.

    When bound to a ViewModel, the validation rules for the bound property will be obtained from the ViewModel and passed to Vuetifyopen in new window's rules prop.

    Examples

    Typical usage, providing an object and a property on that object:

    <c-input :model="person" for="firstName" />
    +

    Customizing the Vuetifyopen in new window component used:

    <c-input :model="comment" for="content" textarea solo />
    +

    Binding to API Caller args objects:

    <c-input 
    +    :model="person.setFirstName" 
    +    for="newName" />
    +

    Or, using a more verbose syntax:

    <c-input 
    +    :model="person.setFirstName.args" 
    +    for="Person.methods.setFirstName.newName" />
    +

    Binding to Data Source Parameters:

    <c-input :model="personList.$dataSource" for="startsWith" />
    +

    Usage with v-model (this scenario is atypical - the model/for pair of props are used in almost all scenarios):

    <c-input v-model="person.firstName" for="Person.firstName" />
    +

    Props

    for?: string | Property | Value

    A metadata specifier for the value being bound. One of:

    • A string with the name of the value belonging to model.
    • A direct reference to a metadata object.
    • A string in dot-notation that starts with a type name.

    model?: Model | DataSource

    An object owning the value that was specified by the for prop. If provided, the input will be bound to the corresponding property on the model object.

    value?: any // Vue 2
    +modelValue?: any // Vue 3

    If binding the component with v-model, accepts the value part of v-model.

    Slots

    default - Used to display fallback content if c-input does not support the type of the value being bound. Generally this does not need to be used, as you should avoid creating c-input components for unsupported types in the first place.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html b/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html new file mode 100644 index 000000000..4fc577e9b --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-list-filters.html @@ -0,0 +1,34 @@ + + + + + + + + + c-list-filters | Coalesce + + + + +

    c-list-filters

    A component that provides an interface for modifying the filters prop of a ListViewModel's parameters.

    Example Usage

    <c-list-filters :list="list" />
    +

    Props

    list: ListViewModel

    The ListViewModel whose filters will be editable.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html b/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html new file mode 100644 index 000000000..8d992496b --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-list-page-size.html @@ -0,0 +1,34 @@ + + + + + + + + + c-list-page-size | Coalesce + + + + +

    c-list-page-size

    A component that provides an dropdown for modifying the pageSize parameter prop of a ListViewModel.

    Example Usage

    <c-list-page-size :list="list" />
    +

    Props

    list: ListViewModel

    The ListViewModel whose pagination will be editable.

    pageSizes?: number[]

    An optional list of available page sizes to offer through c-list-page-size. Defaults to [10, 25, 100].

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html b/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html new file mode 100644 index 000000000..f9878653f --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-list-page.html @@ -0,0 +1,34 @@ + + + + + + + + + c-list-page | Coalesce + + + + +

    c-list-page

    A component that provides previous/next buttons and a text field for modifying the page parameter prop of a ListViewModel.

    Example Usage

    <c-list-page :list="list" />
    +

    Props

    list: ListViewModel

    The ListViewModel whose current page will be changeable with the component.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html b/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html new file mode 100644 index 000000000..c28e5d495 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-list-pagination.html @@ -0,0 +1,34 @@ + + + + + + + + + c-list-pagination | Coalesce + + + + +

    c-list-pagination

    A component that provides an interface for modifying the pagination parameters of a ListViewModel.

    This is a composite of c-list-page-size, c-list-range-display, and c-list-page, arranged horizontally. It is designed to be used above or below a table (e.g. c-table).

    Example Usage

    <c-list-pagination :list="list" />
    +

    Props

    list: ListViewModel

    The ListViewModel whose pagination will be editable.

    pageSizes?: number[]

    An optional list of available page sizes to offer through c-list-page-size. Defaults to [10, 25, 100].

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html b/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html new file mode 100644 index 000000000..512b6976b --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-list-range-display.html @@ -0,0 +1,34 @@ + + + + + + + + + c-list-range-display | Coalesce + + + + +

    c-list-range-display

    Displays pagination information about the current $items of a ListViewModel in the format <start index> - <end index> of <total count>.

    Uses the pagination information returned from the last successful $load call, not the current $params of the ListViewModel.

    Examples

    <c-list-range-display :list="list" />
    +

    Props

    list: ListViewModel

    The ListViewModel to display pagination information for.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html b/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html new file mode 100644 index 000000000..295f62759 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-loader-status.html @@ -0,0 +1,61 @@ + + + + + + + + + c-loader-status | Coalesce + + + + +

    c-loader-status

    A component for displaying progress and error information for one or more API Callers.

    TIP

    It is highly recommended that all API Callers utilized by your application that don't have any other kind of error handling should be represented by a c-loader-status so that users can be aware of any errors that occur.

    Progress is indicated with a Vuetifyopen in new window v-progress-linearopen in new window component, and errors are displayed in a v-alertopen in new window. Transitionsopen in new window are applied to smoothly fade between the different states the the caller can be in.

    Note

    This component uses the legacy term "loader" to refer to API Callers. A new c-caller-status component may be coming in the future with a few usability improvements - if that happens, c-loader-status will be preserved for backwards compatibility.

    Examples

    Wrap contents of a details/edit page:

    <h1>Person Details</h1>
    +<c-loader-status
    +    :loaders="{ 
    +        'no-initial-content no-error-content': [person.$load],
    +        '': [person.$save, person.$delete],
    +    }"
    +    #default
    +>
    +    First Name: {{ person.firstName }}
    +    Last Name: {{ person.lastName }}
    +    Employer: {{ person.company.name }}
    +</c-loader-status>
    +

    Use c-loader-status to render a progress bar and any error messages, but don't use it to control content:

    <c-loader-status :loaders="{'': [list.$load]}" />
    +

    Wrap a save/submit button:

    <c-loader-status
    +    :loaders="{ 'no-loading-content': [person.$save] }"
    +>
    +    <button> Save </button>
    +</c-loader-status>
    +

    Hides the table before the first load has completed, or if loading the list encountered an error. Don't show the progress bar after we've already loaded the list for the first time (useful for loads that occur without user interaction, e.g. setInterval):

    <c-loader-status
    +    :loaders="{
    +        'no-secondary-progress no-initial-content no-error-content': [list.$load]
    +    }"
    +    #default
    +>
    +    <table>
    +        <tr v-for="item in list.$items"> ... </tr>
    +    </table>
    +</c-loader-status>
    +

    Props

    loaders: { [flags: string]: ApiCaller | ApiCaller[] }

    A dictionary object with entries mapping zero or more flags to one or more API Callers. Multiple entries of flags/caller pairs may be specified in the dictionary to give different behavior to different API callers.

    The available flags are as follows. All flags default to true, and may be prefixed with no- to set the flag to false instead of true. Multiple flags may be specified at once by delimiting them with spaces.

    Flag
    Description
    loading-contentControls whether the default slot is rendered while any API caller is loading (i.e. when caller.isLoading === true).
    error-contentControls whether the default slot is rendered while any API Caller is in an error state (i.e. when caller.wasSuccessful === false).
    initial-contentControls whether the default slot is rendered while any API Caller has yet to receive a response for the first time (i.e. when caller.wasSuccessful === null).
    initial-progressControls whether the progress indicator is shown when an API Caller is loading for the very first time (i.e. when caller.wasSuccessful === null).
    secondary-progressControls whether the progress indicator is shown when an API Caller is loading any time after its first invocation (i.e. when caller.wasSuccessful !== null).

    progressPlaceholder: boolean = true

    Specify if space should be reserved for the progress indicator. If set to false, the content in the default slot may jump up and down slightly as the progress indicator shows and hides.

    height: number = 10

    Specifies the height in pixels of the v-progress-linearopen in new window used to indicate progress.

    Slots

    default - Accepts the content whose visibility is controlled by the state of the supplied API Callers. It will be shown or hidden according to the flags defined for each caller.

    TIP

    (Vue 2 Only): Define the default slot as a scoped slotopen in new window (e.g. with #default or v-slot:default on the c-loader-status) to prevent the VNode tree from being created when the content should be hidden. This improves performance and helps avoid null reference errors that can be caused when trying to render objects that haven't been loaded yet.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html b/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html new file mode 100644 index 000000000..945ee5b15 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-select-many-to-many.html @@ -0,0 +1,45 @@ + + + + + + + + + c-select-many-to-many | Coalesce + + + + +

    c-select-many-to-many

    A multi-select dropdown component that allows for selecting values fetched from the generated /list API endpoints for collection navigation properties that were annotated with [ManyToMany].

    TIP

    It is unlikely that you'll ever need to use this component directly - it is highly recommended that you use c-input instead and let it delegate to c-select-many-to-many for you.

    Examples

    <c-select-many-to-many :model="case" for="caseProducts" />
    +
    <c-select-many-to-many 
    +    :model="case" 
    +    for="caseProducts" 
    +    dense
    +    outlined
    +/>
    +
    <c-select-many-to-many 
    +    v-model="case.caseProducts" 
    +    for="Case.caseProducts" 
    +/>
    +

    Props

    for: string | Property | Value

    A metadata specifier for the value being bound. One of:

    • A string with the name of the value belonging to model.
    • A direct reference to a metadata object.
    • A string in dot-notation that starts with a type name.

    Note

    c-select-many-to-many expects metadata for the "real" collection navigation property on a model. If you provide it the string you passed to [ManyToMany], an error wil be thrown.

    model?: Model

    An object owning the value that was specified by the for prop. If provided, the input will be bound to the corresponding property on the model object.

    value?: any // Vue 2
    +modelValue?: any // Vue 3

    If binding the component with v-model, accepts the value part of v-model.

    params?: ListParameters

    An optional set of Data Source Standard Parameters to pass to API calls made to the server.

    Events

    The following events and automatic API calls are only used when bound to a model that has auto-saves enabled.

    • adding - Fired when a new item has been selected, but before the call to /save has completed.
    • added - Fired when the call to /save has completed after adding a new item.
    • deleting - Fired when an item has been removed, but before the call to /delete has completed.
    • deleted - Fired when the call to /delete has completed after removing an item.
    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.html b/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.html new file mode 100644 index 000000000..fb09c3fa9 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-select-string-value.html @@ -0,0 +1,62 @@ + + + + + + + + + c-select-string-value | Coalesce + + + + +

    c-select-string-value

    A dropdown component that will present a list of suggested string values from a custom API endpoint. Allows users to input values that aren't provided by the endpoint.

    Effectively, this is a server-driven autocomplete list.

    Examples

    <c-select-string-value 
    +    :model="person" 
    +    for="jobTitle"
    +    method="getSuggestedJobTitles"
    +/>
    +
    <c-select-string-value 
    +    v-model="title"
    +    label="Job Title"
    +    for="Person"
    +    method="getSuggestedJobTitles"
    +/>
    +
    class Person 
    +{
    +    public int PersonId { get; set; } 
    +
    +    public string JobTitle { get; set; }
    +
    +    [Coalesce]
    +    public static async Task<ICollection<string>> GetSuggestedJobTitles(AppDbContext db, string search) 
    +    {
    +        return await db.People
    +            .Select(p => p.JobTitle)
    +            .Distinct()
    +            .Where(t => t.StartsWith(search))
    +            .OrderBy(t => t)
    +            .Take(100)
    +            .ToListAsync()
    +    }
    +}
    +

    Props

    for: string | Property | Value

    A metadata specifier for the value being bound. One of:

    • A string with the name of the value belonging to model.
    • A direct reference to a metadata object.
    • A string in dot-notation that starts with a type name.

    model: Model

    An object owning the value that was specified by the for prop. If provided, the input will be bound to the corresponding property on the model object.

    method: string

    The camel-cased name of the Custom Method to invoke to get the list of valid values. Will be passed a single string parameter search. Must be a static method on the type of the provided model object that returns a collection of strings.

    params?: DataSourceParameters

    An optional set of Data Source Standard Parameters to pass to API calls made to the server.

    listWhenEmpty?: boolean = false

    True if the method should be invoked and the list displayed when the entered search term is blank.

    eager?: boolean = false

    True if the bound value should be updated as the user types. Otherwise, the bound value is updated when focus is lost or when a suggested value is chosen. This is only applicable for Vuetify 2 - in Vuetify 3, this is the default behavior.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html b/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html new file mode 100644 index 000000000..ad42ec74e --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-select-values.html @@ -0,0 +1,38 @@ + + + + + + + + + c-select-values | Coalesce + + + + +

    c-select-values

    A multi-select input component for collections of non-object values (primarily strings and numbers).

    TIP

    It is unlikely that you'll ever need to use this component directly - it is highly recommended that you use c-input instead and let it delegate to c-select-values for you.

    Examples

    <c-select-values 
    +    :model="post.setTags.args" 
    +    for="Post.methods.setTags.params.tagNames" 
    +/>
    +

    Props

    for: string | CollectionProperty | CollectionValue

    A metadata specifier for the value being bound. One of:

    • A string with the name of the value belonging to model.
    • A direct reference to a metadata object.
    • A string in dot-notation that starts with a type name.

    model?: Model

    An object owning the value that was specified by the for prop.

    value?: any // Vue 2
    +modelValue?: any // Vue 3

    If binding the component with v-model, accepts the value part of v-model.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-select.html b/stacks/vue/coalesce-vue-vuetify/components/c-select.html new file mode 100644 index 000000000..df8159b30 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-select.html @@ -0,0 +1,71 @@ + + + + + + + + + c-select | Coalesce + + + + +

    c-select

    A dropdown component that allows for selecting values fetched from the generated /list API endpoints.

    Used both for selecting values for foreign key and navigation properties, and for selecting arbitrary objects or primary keys independent of a parent or owning object.

    Examples

    Binding to a navigation property or foreign key of a model:

      <c-select :model="person" for="company" />
    +  <!-- OR: -->
    +  <c-select :model="person" for="companyId" />
    +

    Binding an arbitrary primary key value or an arbitrary object:

      <!-- Binding a key: -->
    +  <c-select for="Person" :key-value.sync="selectedPersonId" />
    +
    +  <!-- Binding an object: -->
    +  <c-select for="Person" :object-value.sync="selectedPerson" />
    +  <c-select for="Person" v-model="selectedPerson" />
    +

    Examples of other props:

    <c-select 
    +  for="Person" 
    +  v-model="selectedPerson"
    +  :clearable="false"
    +  preselect-first
    +  :params="{ pageSize: 42, filter: { isActive: true } }"
    +  :create="createMethods"
    +  dense
    +  outlined
    +  color="pink"
    +/>
    +<!-- `createMethods` is defined in the docs of `create` below -->
    +

    Props

    for: string | ForeignKeyProperty | ModelReferenceNavigationProperty | ModelType

    A metadata specifier for the value being bound. One of:

    • The name of a foreign key or reference navigation property belonging to model.
    • The name of a model type.
    • A direct reference to a metadata object.
    • A string in dot-notation that starts with a type name that resolves to a foreign key or reference navigation property.

    TIP

    When binding by a key value, if the corresponding object cannot be found (e.g. there is no navigation property, or the navigation property is null), c-select will automatically attempt to load the object from the server so it can be displayed in the UI.

    model?: Model

    An object owning the value that was specified by the for prop. If provided, the input will be bound to the corresponding property on the model object.

    If for specifies a foreign key or reference navigation property, both the foreign key and the navigation property of the model will be updated when the selected value is changed.

    value?: any // Vue 2
    +modelValue?: any // Vue 3

    When binding the component with v-model, accepts the value part of v-model. If for was specified as a foreign key, this will expect a key; likewise, if for was specified as a type or as a navigation property, this will expect an object.

    keyValue?: any

    When bound with :key-value.sync="keyValue", allows binding the primary key of the selected object explicitly.

    objectValue?: any

    When bound with :object-value.sync="objectValue", allows binding the selected object explicitly.

    clearable?: boolean

    Whether the selection can be cleared or not, emitting null as the input value.

    If not specified and the component is bound to a foreign key or reference navigation property, defaults to whether or not the foreign key has a required validation rule defined in its Metadata.

    preselectFirst?: boolean = false

    If true, then when the first list results for the component are received by the client just after the component is created, c-select will emit the first item in the list as the selected value.

    preselectSingle?: boolean = false

    If true, then when the first list results for the component are received by the client just after the component is created, if the results contained exactly one item, c-select will emit that only item as the selected value.

    reloadOnOpen?: boolean = false

    If true, the list results will be reloaded when the dropdown menu is opened. By default, list results are loaded when the component is mounted and also when any of its parameters change (either search input or the params prop).

    params?: ListParameters

    An optional set of Data Source Standard Parameters to pass to API calls made to the server.

    create?: {
    +  getLabel: (search: string, items: TModel[]) => string | false,
    +  getItem: (search: string, label: string) => Promise<TModel>
    +}

    A object containing a pair of methods that allowing users to create new items from directly within the c-select if a matching object is not found.

    The object must contain the following two methods. You should define these in your component's script section - don't try to define them inline in your component.

    create.getLabel: (search: string, items: TModel[]) => string | false

    A function that will be called with the user's current search term, as well as the collection of currently loaded items being presented to the user as valid selection options.

    It should return either a string that will be presented to the user as an option in the dropdown that can be clicked to invoke the getItem function below, or it should return false to prevent such an option from being shown to the user.

    create.getItem: (search: string, label: string) => Promise<TModel>

    A function that will be invoked when the user clicks the option in the dropdown list described by getLabel. It will be given the user's current search term as well as the value of the label returned from getLabel as parameters. It must perform the necessary operations to create the new object on the server and then return a reference to that object.

    For example:

    createMethods = {
    +  getLabel(search: string, items: Person[]) {
    +    const searchLower = search.toLowerCase();
    +    if (items.some(a => a.name?.toLowerCase().indexOf(searchLower) == 0)) {
    +      return false;
    +    }
    +    return search;
    +  },
    +  async getItem(search: string, label: string) {
    +    const client = new PersonApiClient();
    +    return (await client.addPersonByName(label)).data.object!;
    +  }
    +}
    +

    Slots

    #item="{ item, search }" - Slot used to customize the text of both items inside the list, as well as the text of selected items. By default, items are rendered with c-display. Slot is passed a parameter item containing a model instance, and search containing the current search query..

    #list-item="{ item, search }" - Slot used to customize the text of items inside the list. If not provided, falls back to the item slot.

    #selected-item="{ item, search }" - Slot used to customize the text of selected items. If not provided, falls back to the item slot.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/components/c-table.html b/stacks/vue/coalesce-vue-vuetify/components/c-table.html new file mode 100644 index 000000000..b4a48cf8c --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/components/c-table.html @@ -0,0 +1,51 @@ + + + + + + + + + c-table | Coalesce + + + + +

    c-table

    A table component for displaying the contents of a ListViewModel. Also supports modifying the list's [sort parameters](/modeling/model-components/data-sources.md#standard-parameters) by clicking on column headers. Pairs well with a c-list-pagination.

    Example Usage

    A simple table, rendering the items of a ListViewModel:

    <c-table :list="list" />
    +

    A more complex example using more of the available options:

    <c-table
    +  :list="list"
    +  :props="['firstName', 'lastName']"
    +  :extra-headers="['Actions']"
    +>
    +  <template #item.append="{item}"> 
    +    <td>
    +      <v-btn
    +        title="Edit"
    +        text icon
    +        :to="{name: 'edit-person', params: { id: item.$primaryKey }}"
    +      >
    +        <i class="fa fa-edit"></i>
    +      </v-btn>
    +    </td>
    +  </template>
    +</c-table>
    +

    Props

    list: ListViewModel

    The ListViewModel to display pagination information for.

    props?: string[]

    If provided, specifies which properties, and their ordering, should be given a column in the table.

    If not provided, all non-key columns that aren't annotated with [Hidden(HiddenAttribute.Areas.List)] are given a column.

    extraHeaders?: string[]

    The text contents of one or more extra th elements to render in the table. Should be used in conjunction with the item.append slot.

    editable: boolean = false

    If true, properties in each table cell will be rendered with c-input. Non-editable properties will be rendered in accordance with the value of the admin prop.

    admin: boolean = false

    If true, properties in each table cell will be rendered with c-admin-display instead of c-display.

    Slots

    item.append - A slot rendered after the td elements on each row that render the properties of each item in the table. Should be provided zero or more additional td elements. The number should match the number of additional headers provided to the extraHeaders prop.

    + + + diff --git a/stacks/vue/coalesce-vue-vuetify/overview.html b/stacks/vue/coalesce-vue-vuetify/overview.html new file mode 100644 index 000000000..f29d403d6 --- /dev/null +++ b/stacks/vue/coalesce-vue-vuetify/overview.html @@ -0,0 +1,33 @@ + + + + + + + + + Vuetify Components | Coalesce + + + + +

    Vuetify Components

    open in new windowopen in new window

    The Vueopen in new window stack for Coalesce provides a set of components based on Vuetifyopen in new window, packaged up in an NPM package coalesce-vue-vuetify2open in new window or coalesce-vue-vuetify3open in new window. These components are driven primarily by the Metadata Layer, and include both low level input and display components like c-input and c-display that are highly reusable in the custom pages you'll build in your application, as well as high-level components like c-admin-table-page and c-admin-editor-page that constitute entire pages.

    Setup

    All Coalesce projects should be started from the template described in Getting Started with Vue, and will therefore have all the setup completed for you.

    If for whatever reason you find yourself adding Coalesce to an existing project, use the template as a reference for what configuration needs to be added to your project.

    Display Components

    ComponentDescription

    c-display

    A general-purpose component for displaying any Value by rendering the value to a string with the display functions from the Models Layer. For plain string and number values, usage of this component is largely superfluous. For all other value types including dates, booleans, enums, objects, and collections, it is very handy.

    c-loader-status

    A component for displaying progress and error information for one or more API Callers.

    TIP

    It is highly recommended that all API Callers utilized by your application that don't have any other kind of error handling should be represented by a c-loader-status so that users can be aware of any errors that occur.

    c-list-range-display

    Displays pagination information about the current $items of a ListViewModel in the format <start index> - <end index> of <total count>.

    c-table

    A table component for displaying the contents of a ListViewModel. Also supports modifying the list's [sort parameters](/modeling/model-components/data-sources.md#standard-parameters) by clicking on column headers. Pairs well with a c-list-pagination.

    Input Components

    ComponentDescription

    c-input

    A general-purpose input component for most Values. c-input does not have much functionality of its own - instead, it delegates to the right kind of component based on the type of value to which it is bound. This includes both other Coalesce Vuetify Components as well as direct usages of some Vuetifyopen in new window components.

    c-select

    A dropdown component that allows for selecting values fetched from the generated /list API endpoints.

    Used both for selecting values for foreign key and navigation properties, and for selecting arbitrary objects or primary keys independent of a parent or owning object.

    c-datetime-picker

    A general, all-purpose date/time input component that can be used either with models and metadata or as a standalone component using only v-model.

    c-select-many-to-many

    A multi-select dropdown component that allows for selecting values fetched from the generated /list API endpoints for collection navigation properties that were annotated with [ManyToMany].

    c-select-string-value

    A dropdown component that will present a list of suggested string values from a custom API endpoint. Allows users to input values that aren't provided by the endpoint.

    Effectively, this is a server-driven autocomplete list.

    c-select-values

    A multi-select input component for collections of non-object values (primarily strings and numbers).

    c-list-filters

    A component that provides an interface for modifying the filters prop of a ListViewModel's parameters.

    c-list-pagination

    A component that provides an interface for modifying the pagination parameters of a ListViewModel.

    This is a composite of c-list-page-size, c-list-range-display, and c-list-page, arranged horizontally. It is designed to be used above or below a table (e.g. c-table).

    c-list-page-size

    A component that provides an dropdown for modifying the pageSize parameter prop of a ListViewModel.

    c-list-page

    A component that provides previous/next buttons and a text field for modifying the page parameter prop of a ListViewModel.

    Admin Components

    ComponentDescription

    c-admin-method

    Provides an interface for invoking a method and rendering its result, designed to be use in an admin page.

    c-admin-methods

    Renders in a Vuetifyopen in new window v-expansion-panelsopen in new window a c-admin-method for each method on a ViewModel or ListViewModel.

    c-admin-display

    Behaves the same as c-display, except any collection navigation properties will be rendered as links to an admin list page, and any models will be rendered as a link to an admin item page.

    c-admin-editor

    An editor for a single ViewModel instance. Provides a c-input for each property of the model.

    c-admin-editor-page

    A page for a creating/editing single ViewModel instance. Provides a c-admin-editor and a c-admin-methods for the instance. Designed to be routed to directly with vue-routeropen in new window.

    c-admin-table

    An full-featured table for a ListViewModel, including a c-admin-table-toolbar, c-table, and c-list-pagination.

    c-admin-table-toolbar

    A full-featured toolbar for a ListViewModel designed to be used on an admin page, including "Create" and "Reload" buttons, a c-list-range-display, a c-list-page, a search field, c-list-filters, and a c-list-page-size.

    c-admin-table-page

    A full-featured page for interacting with a ListViewModel. Provides a c-admin-table and a c-admin-methods for the list. Designed to be routed to directly with vue-routeropen in new window.

    + + + diff --git a/stacks/vue/getting-started.html b/stacks/vue/getting-started.html new file mode 100644 index 000000000..ac6368ced --- /dev/null +++ b/stacks/vue/getting-started.html @@ -0,0 +1,77 @@ + + + + + + + + + Getting Started with Vue | Coalesce + + + + +

    Getting Started with Vue

    Creating a Project

    The quickest and easiest way to create a new Coalesce Vue application is to use the dotnet new template. In your favorite shell:

    mkdir MyCompany.MyProject
    +cd MyCompany.MyProject
    +dotnet new install IntelliTect.Coalesce.Vue.Template
    +dotnet new coalescevue
    +cd *.Web
    +npm ci
    +

    open in new window • View on GitHubopen in new window

    Project Structure

    Important

    The Vue template is based on Viteopen in new window. You are strongly encouraged to read through at least the first few pages of the Vite Documentationopen in new window before getting started on any development.

    The structure of the Web project follows the conventions of both ASP.NET Core and Vite. The Vue-specific folders are as follows:

    • /src - Files that should be compiled into your application. CSS/SCSS, TypeScript, Vue SFCs, and so on.
    • /public - Static assets that should be served as files. Includes index.html, the root document of the application.
    • /wwwroot - Target for compiled output.

    During development, no special tooling is required to build your frontend code. Coalesce's UseViteDevelopmentServer in ASP.NET Core will take care of that automatically when the application starts. Just make sure NPM packages have been installed (npm ci).

    Data Modeling

    At this point, you can open up the newly-created solution in Visual Studio and run your application. However, your application won't do much without a data model, so you will probably want to do the following before running:

    • Create an initial Data Model by adding EF entity classes to the data project and the corresponding DbSet<> properties to AppDbContext. You will notice that the starter project includes a single model, ApplicationUser, to start with. Feel free to change this model or remove it entirely. Read Entity Models for more information about creating a data model.

    • Run dotnet ef migrations add Init (Init can be any name) in the data project to create an initial database migration.

    • Run Coalesce's code generation by either:

      • Running dotnet coalesce in the web project's root directory
      • Running the coalesce npm script (Vue) or gulp task (Knockout) in the Task Runner Explorer

    You're now at a point where you can start creating your own pages!

    Building Pages & Features

    Lets say we've created a model called Person as follows, and we've ran code generation with dotnet coalesce:

    namespace MyApplication.Data.Models 
    +{
    +    public class Person
    +    {
    +        public int PersonId { get; set; }
    +        public string Name { get; set; }
    +        public DateTimeOffset? BirthDate { get; set; }
    +    }
    +}
    +

    We can create a details page for a Person by creating a Single File Componentopen in new window in MyApplication.Web/src/views/person-details.vue:

    <template>
    +  <dl>
    +    <dt>Name</dt>
    +    <dd>
    +      <c-display :model="person" for="name" />
    +    </dd>
    +
    +    <dt>Date of Birth</dt>
    +    <dd>
    +      <c-display :model="person" for="birthDate" format="M/d/yyyy" />
    +    </dd>
    +  </dl>
    +</template>
    +
    +<script setup lang="ts"> 
    +import { PersonViewModel } from "@/viewmodels.g";
    +
    +const props = defineProps<{ id: number }>();
    +const person = new PersonViewModel();
    +
    +person.$load(props.id);
    +</script>
    +

    Note

    In the code above, c-display is a component that comes from the Vuetify Components for Coalesce.

    For simple property types like string and number you can always use simple template interpolation syntax, but for more complex properties like dates, c-display is handy to use because it includes features like built-in date formatting.

    We then need to add route to this new view. In MyApplication.Web/src/router.ts, add a new item to the routes array:

    // In the `routes` array, add the following item:
    +{
    +  path: '/person/:id',
    +  name: 'person-details',
    +  component: () => import('@/views/person-details.vue'),
    +  props: route => ({ id: +route.params.id }),
    +},
    +

    With these pieces in place, we now have a functioning page that will display details about a person. We can start up the application (or, if it was already running, refresh the page) and navigate to /person/1 (assuming a person with ID 1 exists - if not, navigate to /admin/Person and create one).

    From this point, you can start adding more fields, more features, and more flair to the page. Check out all the other documentation in the sidebar to see what else Coalesce has to offer, including the Vue Overview.

    + + + diff --git a/stacks/vue/layers/api-clients.html b/stacks/vue/layers/api-clients.html new file mode 100644 index 000000000..d3b7ab9eb --- /dev/null +++ b/stacks/vue/layers/api-clients.html @@ -0,0 +1,90 @@ + + + + + + + + + Vue API Client Layer | Coalesce + + + + +

    Vue API Client Layer

    The API client layer, generated as api-clients.g.ts, exports a class for each API controller that was generated for your data model. These classes are stateless and provide one method for each API endpoint. This includes both the standard set of endpoints created for Entity Models and Custom DTOs, as well as any custom Methods on the aforementioned types, as well as any methods on your Services.

    The API clients provided by Coalesce are based on axiosopen in new window. All API clients used a shared axios instance, exported from coalesce-vue as AxiosClient. This instance can be used to configure all HTTP requests made by Coalesce, including things like attaching interceptorsopen in new window to modify the requests being made, or configuring defaultsopen in new window.

    As with all the layers, the source code of coalesce-vueopen in new window is also a great supplement to this documentation.

    Concepts

    API Client

    A class, generated for each controller-backed type in your data model as <ModelName>ApiClient and exported from api-clients.g.ts containing one method for each API endpoint.

    Each method on the API client takes in the regular parameters of the method as you would expect, as well as an optional AxiosRequestConfig parameter at the end that can be used to provide additional configuration for the single request, if needed.

    For the methods that correspond to the standard set of CRUD endpoints that Coalesce provides (get, list, count, save, delete), an additional parameter parameters is available that accepts the set of Standard Parameters appropriate for the endpoint.

    Each method returns a Promise<AxiosResponse<TApiResult>> where TApiResult is either ItemResult, ItemResult<T>, or ListResult<T>, depending on the return type of the API endpoint. AxiosResponse is the response object from axiosopen in new window, containing the TApiResult in its data property, as well as other properties like headers. The returned type T is automatically converted into valid Model implementations for you.

    API Callers/API States

    A stateful function for invoking an API endpoint, created with the $makeCaller function on an API Client. API Callers provide a wide array of functionality that is useful for working with API endpoints that are utilized by a user interface.

    Because they are such an integral part of the overall picture of coalesce-vue, they have their own section below where they are explained in much greater detail.

    API Callers

    API Callers (typed with the name ApiState in coalesce-vue, sometimes also referred to as "loaders" or "invokers") are stateful functions for invoking an API endpoint, created with the $makeCaller function on an API Client. A summary of features:

    Endpoint Invocation

    Each API Caller is itself a function, so it can be invoked to trigger an API request to the server.

    State management

    API Callers contain properties about the last request made, including things like wasSuccessful, isLoading, result, and more.

    Concurrency Management

    Using setConcurrency(mode), you can configure how each individual caller handles what happens when multiple requests are made simultaneously

    Argument Binding

    API Callers can be created so that they have an args object that can be bound to, using .invokeWithArgs() to make a request using those arguments as the API endpoint's parameters. The API Callers created for the ViewModel Layer are all created this way.

    Creating and Invoking an API Caller

    API Callers can be created with the $makeCaller method of an API Client. The way in which it was created affects how it is invoked, as the parameters that the caller accepts are defined when it is created.

    TIP

    During typical development, it is unlikely that you'll need to make a custom API Caller - the ones created for you on the generated ViewModel Layer will usually suffice. However, creating your own can allow for some more advanced functionality.

    Some examples:

    // Preamble for all the examples below:
    +import { PersonApiClient } from '@/api-clients.g';
    +const client = new PersonApiClient;
    +

    A caller that takes no additional parameters:

    const caller = client.$makeCaller(
    +    "item", 
    +    c => c.namesStartingWith("A")
    +);
    +
    +await caller();
    +console.log(caller.result)
    +

    A caller that takes custom parameters:

    const caller = client.$makeCaller(
    +    methods => methods.namesStartingWith, 
    +    (c, str: string) => c.namesStartingWith(str)
    +);
    +
    +await caller("Rob");
    +console.log(caller.result)
    +

    A caller that has an args object that can be bound to. This is how the generated API Callers in the ViewModel Layer are created:

    const caller = client.$makeCaller("item", 
    +    // The parameter-based version is always required, even if it won't be used.
    +    (c, str: string) => c.namesStartingWith(str),
    +    // A function which creates a blank instance of the args object.
    +    // All props should be initialized (i.e. not undefined) to work with Vue's reactivity.
    +    () => ({str: null as string | null, }),
    +    // The function that accepts the args object and uses it:
    +    (c, args) => c.namesStartingWith(args.str)
    +);
    +
    +caller.args.str = "Su";
    +await caller.invokeWithArgs();
    +console.log(caller.result)
    +

    A caller that performs multiple async operations:

    const deleteFirstNameStartingWith = client.$makeCaller(
    +    "item",
    +    async (c, str: string) => {
    +        const namesResult = await c.namesStartingWith(str)
    +        return await c.deletePersonByName(namesResult.data.object[0])
    +    }
    +);
    +
    +await caller("Rob");
    +console.log(caller.result)
    +

    The first parameter, resultType, can either be one of "item" or "list", indicating whether the method returns a ItemResult or ListResult (examples #1 and #3 above). It can also be a function which accepts the set of method metadata for the API Client and which returns the specific method metadata (example #2 above), or it can be a direct reference to a specific method metadata object.

    Properties

    The following state properties can be found on API Caller instances. These properties are useful for binding to in a user interface to display errors, results, or indicators of progress.

    All Callers

    isLoading: boolean

    True if there is currently a request pending for the API Caller.

    wasSuccessful: boolean | null

    A boolean indicating if the last request made was successful, or null if either no request has been made yet, or if a request has been made but has not yet completed.

    message: string | null

    An error message from the last request, if any. Will be set to null upon successful completion of a request.

    hasResult: boolean

    True if result is non-null. This prop is useful in performance-critical scenarios where checking result directly will cause an overabundance of re-renders in high-churn scenarios.

    args: {}

    Holds an object for the arguments of the function, and will be used if the caller is invoked with its invokeWithArgs() method. Useful for binding the arguments of a caller to inputs in a user interface.

    Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.

    get url(): string

    Returns the URL for the method's HTTP endpoint. Any parameters are sourced from the args object. Useful for binding file-returning HTTP GET methods directly to image or video HTML elements.

    Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.

    ItemResult-based Callers

    result: T | null

    The principal data returned by the previous request. Will be set to null if the last response received returned no data (e.g. if the response was an error response)

    validationIssues: ValidationIssue[] | null

    Any validation issues returned by the previous request. This is never populated automatically by Coalesce, and is therefore is only used if you have written custom code to populate it in your Behaviors or Methods.

    ListResult-based Callers

    result: Array<T> | null

    The principal data returned by the previous request. Will be set to null if the last response received returned no data (e.g. if the response was an error response).

    page, pageSize, pageCount, totalCount: number | null

    Properties which contain the pagination information returned by the previous request.

    Concurrency Mode

    setConcurrency(mode: 'disallow' | 'debounce' | 'cancel' | 'allow')

    API callers have a setConcurrency method that allows you to customize how they behave when additional invocations are performed when there is already a request pending. There are four options available, with "disallow" being the default:

    "disallow"

    The default behavior - simply throws an error for any secondary invocations.

    Note

    Having "disallow" as the default prevents the unexpected behavior that can happen in a number of ways with the other modes:

    • For requests that are performing data-mutating actions on the server, all other concurrency modes could lead to an unexpected end state of the data due to requests either being abandoned, cancelled, or potentially happening out-of-order.
    • Throwing errors for multiple concurrent requests quickly surfaces issues during development where concurrent requests are not being correctly guarded against in a user interface - e.g. not disabling a "Save" or "Submit" button while the request is pending, which would otherwise lead to double-posts.
    "debounce"

    When a secondary invocation is performed, enqueue it after the current pending invocation completes.

    If additional invocations are performed while there is already an invocation enqueued and waiting, the already-enqueued invocation is abandoned and replaced by the most recent invocation attempt. The promise of the abandoned invocation will be resolved with undefined (it is NOT rejected).

    "cancel"

    When a secondary invocation is performed, cancel the current pending invocation.

    This completely aborts the request, propagating all the way back to the server where cancellation can be observed with HttpContext.RequestAbortedopen in new window. The promise of the cancelled invocation will be resolved with undefined (it is NOT rejected).

    "allow"

    When a secondary invocation is performed, always continue normally, sending the request to the server.

    The state of the properties on the caller at any time will reflect the most recent response received from the server, which is never guaranteed to correlate with the most recent request made to the server - that is, requests are not guaranteed to complete in the order they were made. In particular, the isLoading property will be false after the first response comes back, even if the second response has not yet been received.

    WARNING

    For the reasons outlined above, it is generally not recommended to use "allow" unless you fully understand the drawbacks. This mode mirrors the legacy behavior of the Knockout stack for Coalesce.

    Response Caching

    Response caching on API Callers is a feature that will save API responses to persistent storage (sessionStorage or localStorage). The next time a matching request is made, the result property of the API Caller will be populated with that saved response, allowing for a faster time to interactivity and reduced repaints and shifting of elements as initial data loads after a page navigation. It does not prevent any HTTP requests from being made, and does not affect the Promise returned from invoke or invokeWithArgs.

    Common use cases include:

    • Site-wide status or alert messages
    • Server-provided configuration
    • Dashboard data, like statistics or graphs

    When a cached response is loaded, result is populated with that response's data, wasSuccessful and hasResult are set to true, and onFulfilled callbacks are invoked.

    useResponseCaching(configuration?: ResponseCachingConfiguration | false)

    Enables response caching on the API Caller. Only HTTP GET methods are supported, and file-returning methods are not supported. Call with false to disable caching after it was previously enabled. The available options are as follows:

    export type ResponseCachingConfiguration = {
    +  /** Function that will determine the cache key used for a particular request.
    +   * Return a falsy value to prevent caching. The default key is the request URL.
    +   */
    +  key?: (
    +    req: AxiosRequestConfig,
    +    defaultKey: string
    +  ) => string | null | undefined;
    +
    +  /** The maximum age of a cached response. If null, the entry will not expire. Default 1 hour.
    +   *
    +   * The smallest of the current configured max age and the max age that was set at the time of the cached response is used. */
    +  maxAgeSeconds?: number | null;
    +
    +  /** The Storage (default `sessionStorage`) that will hold cached responses. */
    +  storage?: Storage;
    +};
    +

    Other Methods

    API Callers have a few other methods available as well:

    cancel(): void

    Manually cancel the current request. The promise of the cancelled invocation will be resolved with undefined (it is NOT rejected). If using concurrency mode "allow", only the most recent invocation is cancelled.

    onFulfilled((state: TInvoker) => void | Promise<any>): void

    Add a callback to the caller to be invoked when a success response is received from the server. If a promise is returned, this promise will be awaited and will delay the setting of the isLoading prop to false until it completes.

    onRejected((state: TInvoker) => void | Promise<any>): void

    Add a callback to the caller to be invoked when a failure response is received from the server. If a promise is returned, this promise will be awaited and will delay the setting of the isLoading prop to false until it completes.

    invoke(...args: TArgs)

    The invoke function is a reference from the caller to itself. In other words, caller.invoke === caller. This exists to mirror the syntax of the Knockout generated method classes.

    invokeWithArgs(args?: {})

    If called a parameter, that parameter will be used as the args object. Otherwise, caller.args will be used.

    Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.

    getResultObjectUrl(vue?: Vue): string | undefined

    If the method returns a file, this method will return an Object URLopen in new window representing the value of the result prop.

    Accepts a Vue instance in order to manage the lifecycle of the URL, since object URLs must be manually released to avoid memory leaks. When the provided Vue component is destroyed, the object URL will be destroyed. If called inside the component template, the Vue instance can be acquired automatically.

    Only exists if the caller was created with the option of being invoked with an args object as described in the sections above.

    + + + diff --git a/stacks/vue/layers/metadata.html b/stacks/vue/layers/metadata.html new file mode 100644 index 000000000..138fe8a67 --- /dev/null +++ b/stacks/vue/layers/metadata.html @@ -0,0 +1,33 @@ + + + + + + + + + Vue Metadata Layer | Coalesce + + + + +

    Vue Metadata Layer

    The metadata layer, generated as metadata.g.ts, contains information about the types, properties, methods, and other components of your data model. Because Vue applications are typically compiled into a set of static assets, it is necessary for the frontend code to have a representation of your data model as an analog to the ReflectionRepository that is available at runtime in your .NET app.

    Concepts

    The following is a non-exhaustive list of the general concepts used by the metadata layer. The source code of coalesce-vueopen in new window provides the most exhaustive set of documentation about the metadata layer:

    Metadata

    All objects in the metadata layer that represent any kind of metadata have, at the very least, a name, the name of the metadata element in code (type names, property names, parameter names, etc). and a displayName, the human-readable form of the name that is suitable for presentation when needed. Names follow the casing convention of their corresponding language elements - types are PascalCased, while other things like properties, methods, and parameters are camelCased.

    Type

    All custom types exposed by your application's data model will have a Type metadata object generated. This includes both C# classes, and C# enums. Class types include model (for Entity Models and Custom DTOs) and object (for External Types).

    Value

    In the metadata layer, a Value is the usage of a type. This could be any type - strings, numbers, enums, classes, or even void. Values can be found in the collection of an object's properties, a method's parameters or return value, or as a data source's parameters.

    All values have the following properties:

    type: TypeDiscriminator

    Type could be a language primitive like string or number, a non-primitive JavaScript type (date, file), or in the case of a custom Type, the type kind of that type (model, enum, object). For custom types, an additional property typeDef will refer to the Type metadata for that type.

    role: ValueRole

    Role represents what purpose the value serves in a relational model. Either value (the default - no relational role), primaryKey, foreignKey, referenceNavigation, or collectionNavigation.

    Property

    A Property is a more refined Value that contains a number of additional fields based on the role of the property.

    Domain

    The type of the default export of the generated metadata. Serves as a single root from which all other metadata can be accessed. Contains fields types, enums, and services as organizing structures for the different kinds of custom types.

    + + + diff --git a/stacks/vue/layers/models.html b/stacks/vue/layers/models.html new file mode 100644 index 000000000..7cdf59d1a --- /dev/null +++ b/stacks/vue/layers/models.html @@ -0,0 +1,115 @@ + + + + + + + + + Vue Model Layer | Coalesce + + + + +

    Vue Model Layer

    The model layer, generated as models.g.ts, contains a set of TypeScript interfaces that represent each client-exposed type in your data model. Each interface contains all the Properties of that type, as well as a $metadata property that references the metadata object for that type. Enums and Data Sources are also represented in the model layer.

    The model layer also includes a TypeScript class for each type that can be used to easily instantiate a valid implementation of its corresponding interface. However, it is not necessary for the classes to be used, and all parts of Coalesce that interact with the model layer don't perform any instanceof checks against models - the $metadata property is used to determine type identity.

    Concepts

    The model layer is fairly simple - the only main concept it introduces on top of the Metadata Layer is the notion of interfaces and enums that mirror the C# types in your data model. As with the Metadata Layer, the source code of coalesce-vueopen in new window is a great documentation supplement to this page.

    Model

    An interface describing an instance of a class type from your application's data model. All Model interfaces contain members for all the Properties of that type, as well as a $metadata property that references the metadata object for that type.

    DataSource

    A class-based representation of a Data Source containing properties for any of the Custom Parameters of the data source, as well as a $metadata property that references the metadata object for the data source.

    Data sources are generated as concrete classes in a namespace named DataSources that is nested inside a namespace named after their parent model type. For example:

    import { Person } from '@/models.g'
    +
    +const dataSource = new Person.DataSources.NamesStartingWith;
    +dataSource.startsWith = "A";
    +// Provide the dataSource to an API Client or a ViewModel...
    +

    Model Functions

    The following functions exported from coalesce-vue can be used with your models:

    // Vue Options API
    +bindToQueryString(vue: Vue, obj: {}, key: string, queryKey: string = key, parse?: (v: any) => any, mode: 'push' | 'replace' = 'replace')
    + 
    +// Vue Composition API
    +useBindToQueryString(obj: {}, key: string, queryKey: string = key, parse?: (v: any) => any, mode: 'push' | 'replace' = 'replace')

    Binds property key of obj to query string parameter queryKey. When the object's value changes, the query string will be updated using vue-routeropen in new window. When the query string changes, the object's value will be updated.

    The query string will be updated using either router.push or router.replace depending on the value of parameter mode.

    If the query string contains a value when this is called, the object will be updated with that value immediately.

    If the object being bound to has $metadata, information from that metadata will be used to serialize and parse values to and from the query string. Otherwise, String(value) will be used to serialize the value, and the parse parameter (if provided) will be used to parse the value from the query string.

    import { bindToQueryString } from 'coalesce-vue';
    +
    +// In the 'created' Vue lifecycle hook on a component:
    +created() {
    +    // Bind pagination information to the query string:
    +    bindToQueryString(this, this.listViewModel.$params, 'pageSize', 'pageSize', v => +v);
    +
    +    // Assuming the component has an 'activeTab' data member:
    +    bindToQueryString(this, this, 'activeTab');
    +}
    +

    // Vue Options API
    +bindKeyToRouteOnCreate(vue: Vue, model: Model<ModelType>, routeParamName: string = 'id', keepQuery: boolean = false)
    + 
    +// Vue Composition API
    +useBindKeyToRouteOnCreate(model: Model<ModelType>, routeParamName: string = 'id', keepQuery: boolean = false)

    When model is created (i.e. its primary key becomes non-null), replace the current URL with one that includes uses primary key for the route parameter named by routeParamName.

    The query string will not be kept when the route is changed unless true is given to keepQuery.

    import { bindKeyToRouteOnCreate } from 'coalesce-vue';
    +
    +// In the 'created' Vue lifecycle hook on a component:
    +created() {
    +    if (this.id) {
    +        this.viewModel.$load(this.id);
    +    } else {
    +        bindKeyToRouteOnCreate(this, this.viewModel);
    +    }
    +}
    +

    Note

    The route will be replaced directly via the HTML5 History APIopen in new window such that vue-routeropen in new window will not observe the change as an actual route change, preventing the current view from being recreated if a path-based key is being used on the application's <router-view> component.

    Advanced Model Functions

    The following functions exported from coalesce-vue can be used with your models.

    Note

    These functions are used to implement the higher-order layers in the Vue stack.

    While you're absolutely free to use them in your own code and can rely on their interface and behavior to remain consistent, you will find that you seldom need to use them directly - that's why we've split them into their own section here in the documentation.

    convertToModel(value: any, metadata: Value | ClassType): any

    Given any JavaScript value value, convert it into a valid implementation of the value or type described by metadata.

    For metadata describing a primitive or primitive-like value, the input will be parsed into a valid implementation of the correct JavaScript type. For example, for metadata that describes a boolean, a string "true" will return a boolean true, and ISO 8601 date strings will result in a JavaScript Date object.

    For metadata describing a type, the input object will be mutated into a valid implementation of the appropriate model interface. Missing properties will be set to null, and any descendent properties of the provided object will be recursively processed with convertToModel.

    If any values are encountered that are fundamentally incompatible with the requested type described by the metadata, an error will be thrown.

    mapToModel(value: any, metadata: Value | ClassType): any

    Performs the same operations as convertToModel, except that any objects encountered will not be mutated - instead, a new object or array will always be created.

    mapToDto(value: any, metadata: Value | ClassType): any

    Maps the input to a representation suitable for JSON serialization.

    Will not serialize child objects or collections whose metadata includes dontSerialize. Will only recurse to a maximum depth of 3.

    modelDisplay(model: Model, options?: DisplayOptions): string

    Returns a string representing the model suitable for display in a user interface.

    Uses the displayProp defined on the object's metadata. If no displayProp is defined, the object will be displayed as JSON. The display prop on a model can be defined in C# with [ListText].

    See DisplayOptions for available options.

    propDisplay(model: Model, prop: Property | string, options?: DisplayOptions): string

    Returns a string representing the specified property of the given object suitable for display in a user interface.

    The property can either be a string, representing one of the model's properties, or the actual Property metadata object of the property.

    See DisplayOptions for available options.

    valueDisplay(value: any, metadata: Value, options?: DisplayOptions): string

    Returns a string representing the given value (described by the given metadata).

    See DisplayOptions for available options.

    DisplayOptions

    The following options are available to functions in coalesce-vue that render a value or object for display:

    export interface DisplayOptions {
    +  /** Date format options. One of:
    +   * - A UTS#35 date format string (https://date-fns.org/docs/format)
    +   * - An object with options for https://date-fns.org/docs/format or https://github.com/marnusw/date-fns-tz#format, including a string `format` for the format itself. If a `timeZone` option is provided per https://github.com/marnusw/date-fns-tz#format, the date being formatted will be converted to that timezone.
    +   * - An object with options for https://date-fns.org/docs/formatDistance */
    +  format?:
    +    | string
    +    | ({
    +        /** A UTS#35 date format string (https://date-fns.org/docs/format) */
    +        format: string;
    +      } & Parameters<typeof format>[2])
    +    | {
    +        /** Format date with https://date-fns.org/docs/formatDistanceToNow */
    +        distance: true;
    +        /** Append/prepend `'in'` or `'ago'` if date is after/before now. Default `true`. */
    +        addSuffix?: boolean;
    +        /** Include detail smaller than one minute. Default `false`. */
    +        includeSeconds?: boolean;
    +      };
    +
    +  collection?: {
    +    /** The maximum number of items to display individually.
    +     * When there are more than this number of items, the count of items will be displayed instead.
    +     * Default `5`.
    +     * */
    +    enumeratedItemsMax?: number;
    +
    +    /** The separator to place between enumerated items. Default `', '` */
    +    enumeratedItemsSeparator?: string;
    +  };
    +}
    +

    Note

    Dates rendered with the formatDistanceToNow function into a Vue component will not automatically be updated in realtime. If this is needed, you should use a strategy like using a keyopen in new window that you periodically update to force a re-render.

    Time Zones

    In Coalesce Vue, all DateTimeOffset-based properties, for both inputs and display-only contexts, are by default formatted into the user's computer's system time zone. This is largely just a consequence of how the JavaScript Date type works. However, this behavior can be overridden by configuring a global default timezone, or by providing a time zone name to individual usages.

    Fields with a type of DateTime are agnostic to time zone and UTC offset and so are not subject to any of the following rules.

    setDefaultTimeZone(timeZoneName: string | null): void

    Gets or sets the default time zone used by Coalesce. The time zone should be an IANA Time Zone Databaseopen in new window name, e.g. "America/Los_Angeles".

    The time zone provided here is used in the following ways:

    • It will be used as DisplayOptions.format.timeZone if no other value was provided for this option. This is used by functions modelDisplay, propDisplay, and valueDisplay, as well as the c-display component.
    • It will be used by c-datetime-picker, used to both interpret the user input and display the selected date. This can also be set on individual component usages via the timeZone prop.
    • It will be used when serializing DateTimeOffset fields into JSON DTOs, representing the ISO 8601 date string in the specified time zone rather than in the user's computer's system time zone.

    getDefaultTimeZone(): string | null

    Returns the current configured default time zone. Default is null, falling back on the user's computer's system time zone.

    + + + diff --git a/stacks/vue/layers/viewmodels.html b/stacks/vue/layers/viewmodels.html new file mode 100644 index 000000000..14135a466 --- /dev/null +++ b/stacks/vue/layers/viewmodels.html @@ -0,0 +1,77 @@ + + + + + + + + + Vue ViewModel Layer | Coalesce + + + + +

    Vue ViewModel Layer

    The ViewModel layer, generated as viewmodels.g.ts, exports a ViewModel class for each API-backed type in your data model (Entity Models, Custom DTOs, and Services). It also exports a ListViewModel type for Entity Models and Custom DTOs.

    These classes provide a wide array of functionality that is useful when interacting with your data model through a user interface. The generated ViewModels are the primary way that Coalesce is used when developing a Vue application.

    ViewModels

    The following members can be found on the generated Entity and Custom DTO ViewModels, exported from viewmodels.g.ts as <TypeName>ViewModel.

    Model Data Properties

    Each ViewModel class implements the corresponding interface from the Model Layer, meaning that the ViewModel has a data property for each Property on the model. Object-typed properties will be typed as the corresponding generated ViewModel.

    Changing the value of a property will automatically flag that property as dirty. See Auto-save & Dirty Flags below for information on how property dirty flags are used.

    There are a few special behaviors when assigning to different kinds of data properties on View Models as well:

    Model Object Properties

    • If the object being assigned to the property is not a ViewModel instance, a new instance will be created automatically and used instead of the incoming object.
    • If the model property is a reference navigation, the corresponding foreign key property will automatically be set to the primary key of that object. If the incoming value was null, the foreign key will be set to null.
    • If deep auto-saves are enabled on the instance being assigned to, auto-save will be spread to the incoming object, and to all other objects reachable from that object.

    Model Collection Properties

    • When assigning an entire array, any items in the array that are not a ViewModel instance will have an instance created for them.
    • The same rule goes for pushing items into the existing array for a model collection - a new ViewModel instance will be created and be used instead of the object(s) being pushed.

    Foreign Key Properties

    If the corresponding navigation property contains an object, and that object's primary key doesn't match the new foreign key value being assigned, the navigation property will be set to null.

    Other Data Properties & Functions

    readonly $metadata: ModelType

    The metadata object from the Metadata Layer layer for the type represented by the ViewModel.

    readonly $stableId: number

    An immutable number that is unique among all ViewModel instances, regardless of type.

    Useful for uniquely identifying instances with :key="vm.$stableId" in a Vue component, especially for instances that lack a primary key.

    $primaryKey: string | number

    A getter/setter property that wraps the primary key of the model. Used to interact with the primary key of any ViewModel in a polymorphic way.

    $display(prop?: string | Property): string

    Returns a string representation of the object, or one of its properties if specified, suitable for display.

    $addChild(prop: string | ModelCollectionNavigationProperty, initialDirtyData?: {})

    Creates a new instance of an item for the specified child model collection, adds it to that collection, and returns the item. If initialDirtyData is provided, it will be loaded into the new instance with $loadDirtyData().

    Loading & Parameters

    $load: ItemApiState;
    +$load(id?: TKey) => ItemResultPromise<TModel>;

    An API Caller for the /get endpoint. Accepts an optional id argument - if not provided, the ViewModel's $primaryKey is used instead. Uses the instance's $params object for the Standard Parameters.

    $params: DataSourceParameters

    An object containing the Standard Parameters to be used for the $load, $save, $bulkSave, and $delete API callers.

    $dataSource: DataSource

    Getter/setter wrapper around $params.dataSource. Takes an instance of a Data Source class generated in the Model Layer.

    $includes: string | null

    Getter/setter wrapper around $params.includes. See Includes String for more information.

    $loadCleanData(source: {} | TModel, purgeUnsaved = false)

    Loads data from the provided model into the current ViewModel, and then clears all dirty flags.

    Data is loaded recursively into all related ViewModel instances, preserving existing instances whose primary keys match the incoming data.

    If auto-save is enabled, only non-dirty properties are updated. This prevents user input that is pending a save from being overwritten by the response from an auto-save /save request.

    If purgeUnsaved is true, items without a primary key will be dropped from collection navigation properties. This is used by the $load caller in order to fully reset the object graph with the state from the server.

    $loadDirtyData(source: {} | TModel)

    Same as $loadCleanData, but does not clear any existing dirty flags, nor does it clear any dirty flags that will be set while mutating the data properties of any ViewModel instance that gets loaded.

    constructor(initialDirtyData?: {} | TModel | null)

    Create a new instance of the ViewModel, loading the given initial data with $loadDirtyData() if provided.

    Saving and Deleting

    $save: ItemApiState;
    +$save(overrideProps?: Partial<TModel>) => ItemResultPromise<TModel>;

    An API Caller for the /save endpoint. Uses the instance's $params object for the Standard Parameters. A save operation saves only properties on the model it is called on - for deep/bulk saves, see $bulkSave.

    This caller is used for both manually-triggered saves in custom code and for auto-saves. If the Rules/Validation report any errors when the caller is invoked, an error will be thrown.

    overrideProps can provide properties to save that override the data properties on the ViewModel instance. This allows for manually saving a change to a property without setting the property on the ViewModel instance into a dirty state. This makes it easier to handle some scenarios where changing the value of the property may put the UI into a logically inconsistent state until the save response has been returned from the server - for example, if a change to one property affects the computed value of other properties.

    When a save creates a new record and a new primary key is returned from the server, any entities attached to the current ViewModel via a collection navigation property will have their foreign keys set to the new primary key. This behavior, combined with the usage of deep auto-saves, allows for complex object graphs to be constructed even before any model in the graph has been created.

    When a save is in progress, the names of properties being saved are in contained in $savingProps.

    Saving behavior can be further customized with $loadResponseFromSaves and $saveMode, listed below.

    $delete: ItemApiState;
    +$delete() => ItemResultPromise<TModel>;

    An API Caller for the /delete endpoint. Uses the instance's $params object for the Standard Parameters.

    If the object was loaded as a child of a collection, it will be removed from that collection upon being deleted. Note that ViewModels currently only support tracking of a single parent collection, so if an object is programmatically added to additional collections, it will only be removed from one of them upon delete.

    $loadResponseFromSaves: boolean

    Default true - controls if a ViewModel will be loaded with the data from the model returned by the /save endpoint when saved with the $save API caller. There is seldom any reason to disable this.

    $savingProps: ReadonlySet<string>

    When $save.isLoading == true, contains the properties of the model currently being saved by $save (including auto-saves). Does not include non-dirty properties even if $saveMode == 'whole'.

    This can be used to make per-property UI state changes during saves - for example, displaying progress indicators on/near individual inputs, or disabling input controls.

    $saveMode: 'surgical' | 'whole'

    Configures which properties of the model are sent to the server during a save or bulk save.

    "surgical" (default)

    By default, only dirty properties (and always the primary key) are sent to the server when performing a save.

    This improves the handling of concurrent changes being made by multiple users against different fields of the same entity at the same time - specifically, it prevents a user with a stale value of some field X from overwriting a more recent value of X in the database when the user is only making changes to some other property Y and has no intention of changing X.

    Save mode "surgical" doesn't help when multiple users are editing field X at the same time - if such a scenario is applicable to your application, you must implement more advanced handling of concurrency conflictsopen in new window.

    WARNING

    Surgical saves require DTOs on the server that are capable of determining which of their properties have been set by the model binder, as surgical saves are sent from the client by entirely omitting properties from the x-www-form-urlencoded body that is sent to the server.

    The Generated C# DTOs implement the necessary logic for this; however, any Custom DTOs must have this logic manually written by you, the developer. Either implement the same pattern that can be seen in the Generated C# DTOs, or do not use surgical saves with Custom DTOs.

    "whole"

    All serializable properties of the object are sent back to the server with every save.

    $getPropDirty(propName: string): boolean

    Returns true if the given property is flagged as dirty.

    $setPropDirty(propName: string, dirty: boolean = true, triggerAutoSave = true)

    Manually set the dirty flag of the given property to the desired state. This seldom needs to be done explicitly, as mutating a property will automatically flag it as dirty.

    If dirty is true and triggerAutoSave is false, auto-save (if enabled) will not be immediately triggered for this specific flag change. Note that a future change to any other property's dirty flag will still trigger a save of all dirty properties.

    $isDirty: boolean

    Getter/setter that summarizes the model's property-level dirty flags. Returns true if any properties are dirty.

    When set to false, all property dirty flags are cleared. When set to true, all properties are marked as dirty.

    Auto-save

    // Vue Options API
    +$startAutoSave(vue: Vue, options: AutoSaveOptions<this> = {})
    + 
    +// Vue Composition API
    +$useAutoSave(options: AutoSaveOptions<this> = {})

    Starts auto-saving of the instance when its savable data properties become dirty. Saves are performed with the $save API Caller (documented above) and will not be performed if the ViewModel has any validation errors - see Rules/Validation below.

    type AutoSaveOptions<TThis> = 
    +{ 
    +    /** Time, in milliseconds, to debounce saves for.  */
    +    wait?: number;
    +    
    +    /** If true, auto-saving will also be enabled for all view models that are
    +        reachable from the navigation properties & collections of the current view model. */
    +    deep?: boolean;
    +
    +    /** Additional options to pass to the third parameter of lodash's `debounce` function. */
    +    debounce?: DebounceSettings;
    +
    +    /** A function that will be called before autosaving that can return false to prevent a save. 
    +        Only allowed if not using deep auto-saves.
    +    */
    +    predicate?: (viewModel: TThis) => boolean;
    +}
    +

    $stopAutoSave(): void

    Turns off auto-saving of the instance. Does not recursively disable auto-saves on related instances if deep was used when auto-save was enabled.

    readonly $isAutoSaveEnabled: boolean

    Returns true if auto-save is currently active on the instance.

    Bulk saves

    $bulkSave: ItemApiState;
    +$bulkSave() => ItemResultPromise<TModel>;

    Bulk saves save all changes to an object graph in one API call and one database transaction. This includes creation, updates, and deletions of entities.

    To use bulk saves, you can work with your ViewModel instances on the client much in the same way you would on the server with Entity Framework. Assign objects to reference navigation properties and modify scalar values to perform creates and updates. To perform deletions, you must call model.$remove() on the ViewModel you want to remove, similar how you would call DbSet<>.Remove(model) on the server.

    If the client-side Rules/Validation report any errors for any of the models being saved in the operation, an error will be thrown.

    On the server, each affected entity is handled through the same standard mechanisms as are used by individual saves or deletes (Behaviors, Data Sources, and Security Attributes), but with a bit of sugar on top:

    • All operations are wrapped in a single database transaction that is rolled back if any individual operation fails.
    • Foreign keys will be fixed up as new items are created, allowing a parent and child record to be created at the same time even when the client has no foreign key to link the two together.

    For the response to a bulk save, the server will load and return the root ViewModel that $bulkSave was called upon, using the instance's $params object for the Standard Parameters.

    $remove(): void

    Removes the item from its parent collection (if it is in a collection), and marks the item for deletion in the next bulk save.

    readonly $isRemoved: boolean

    Returns true if the instance was previously removed by calling $remove().

    Rules/Validation

    $addRule(prop: string | Property, identifier: string, rule: (val: any) => true | string)

    Add a custom validation rule to the ViewModel for the specified property. identifier should be a short, unique slug that describes the rule; it is not displayed in the UI, but is used if you wish to later remove the rule with $removeRule().

    The function you provide should take a single argument that contains the current value of the property, and should either return true to indicate that the validation rule has succeeded, or a string that will be displayed as an error message to the user.

    Any failing validation rules on a ViewModel will prevent that ViewModel's $save caller from being invoked.

    $removeRule(prop: string | Property, identifier: string)

    Remove a validation rule from the ViewModel for the specified property and rule identifier.

    This can be used to remove either a rule that was provided by the generated Metadata Layer, or a custom rule that was added by $addRule. Reference your generated metadata file metadata.g.ts to see any generated rules and the identifiers they use.

    $getRules(prop: string | Property): ((val: any) => string | true)[]

    Returns an array of active rule functions for the specified property, or undefined if the property has no active validation rules.

    $getErrors(prop?: string | Property): Generator<string>

    Returns a generatoropen in new window that provides all error messages for either a specific property (if provided) or the entire model (if no prop argument is provided).

    TIP

    You can obtain an array from a generator with Array.from(vm.$getErrors()) or [...vm.$getErrors()]

    readonly $hasError: boolean

    Indicates if any properties have validation errors.

    Generated Members

    API Callers

    For each of the instance Methods of the type, an API Caller will be generated.

    addTo*() Functions

    For each collection navigation property, a method is generated that will create a new instance of the ViewModel for the collected type, add it to the collection, and then return the new object.

    Many-to-many helper collections

    For each collection navigation property annotated with [ManyToMany], a getter-only property is generated that returns a collection of the object on the far side of the many-to-many relationship. Nulls are filtered from this collection.

    ListViewModels

    The following members can be found on the generated ListViewModels, exported from viewmodels.g.ts as *TypeName*ListViewModel.

    Data Properties

    readonly $items: T[]

    Collection holding the results of the last successful invocation of the $load API Caller.

    Parameters & API Callers

    $params: ListParameters

    An object containing the Standard Parameters to be used for the $load and $count API callers.

    $dataSource: DataSource

    Getter/setter wrapper around $params.dataSource. Takes an instance of a Data Source class generated in the Model Layer.

    $includes: string | null

    Getter/setter wrapper around $params.includes. See Includes String for more information.

    $load: ListApiState;
    +$load() => ListResultPromise<TModel>

    An API Caller for the /list endpoint. Uses the instance's $params object for the Standard Parameters.

    Results are available in the $items property. The result property of the $load API Caller contains the raw results and is not recommended for use in general development - $items should always be preferred.

    $count: ItemApiState;
    +$count() => ItemResultPromise<number>

    An API Caller for the /count endpoint. Uses the instance's $params object for the Standard Parameters.

    The result is available in $count.result - this API Caller does not interact with other properties on the ListViewModel like $pageSize or $pageCount.

    readonly $hasPreviousPage: boolean 
    +readonly $hasNextPage: boolean

    Properties which indicate if $page can be decremented or incremented, respectively. $pageCount and $page are used to make this determination.

    $previousPage(): void 
    +$nextPage(): void

    Methods that will decrement or increment $page, respectively. Each does nothing if there is no previous or next page as returned by $hasPreviousPage and $hasNextPage.

    $page: number

    Getter/setter wrapper for $params.page. Controls the page that will be requested on the next invocation of $load.

    $pageSize: number

    Getter/setter wrapper for $params.pageSize. Controls the page that will be requested on the next invocation of $load.

    readonly $pageCount: number

    Shorthand for $load.pageCount - returns the page count reported by the last successful invocation of $load.

    Auto-Load

    // Vue Options API
    +$startAutoLoad(vue: Vue, options: AutoLoadOptions<this> = {})
    + 
    +// Vue Composition API
    +$useAutoLoad(options: AutoLoadOptions<this> = {})

    Starts auto-loading of the list as changes to its parameters occur. Loads are performed with the $load API Caller.

    type AutoLoadOptions<TThis> =
    +{ 
    +    /** Time, in milliseconds, to debounce loads for.  */
    +    wait?: number;
    +
    +    /** Additional options to pass to the third parameter of lodash's `debounce` function. */
    +    debounce?: DebounceSettings;
    +
    +    /** A function that will be called before loading that can return false to prevent a load. */
    +    predicate?: (viewModel: TThis) => boolean;
    +}
    +

    $stopAutoLoad()

    Manually turns off auto-loading of the instance.

    Generated Members

    API Callers

    For each of the static Methods on the type, an API Caller will be created.

    Service ViewModels

    The following members can be found on the generated Service ViewModels, exported from viewmodels.g.ts as <ServiceName>ViewModel.

    Generated Members

    API Callers

    For each method of the Service, an API Caller will be created.

    + + + diff --git a/stacks/vue/overview.html b/stacks/vue/overview.html new file mode 100644 index 000000000..e5fa13b61 --- /dev/null +++ b/stacks/vue/overview.html @@ -0,0 +1,33 @@ + + + + + + + + + Vue Overview | Coalesce + + + + +

    Vue Overview

    The Vue stack for Coalesce has been designed from the ground up to be used to build modern web applications using current technologies like Vite or Webpack + Vue CLI, ES Modules, and more. It enables you to use all of the features of Vue.js, including building a SPA, and the ability to use modern component frameworks like Vuetifyopen in new window.

    Getting Started

    Check out Getting Started with Vue to learn how to get a new Coalesce Vue project up and running.

    TypeScript Layers

    open in new window

    The generated code for the Vue stack all builds on the coalesce-vueopen in new window NPM package which contains most of the core functionality of the Vue stack. Its version should generally be kept in sync with the IntelliTect.Coalesce NuGet packagesopen in new window in your project.

    Both the generated code and coalesce-vueopen in new window are split into four layers, with each layer building on the layers underneath. From the bottom, these layers are:

    Metadata Layer

    The metadata layer, generated as metadata.g.ts, contains information about the types, properties, methods, and other components of your data model. Because Vue applications are typically compiled into a set of static assets, it is necessary for the frontend code to have a representation of your data model as an analog to the ReflectionRepository that is available at runtime in your .NET app.

    Read more about the Metadata layer

    Model Layer

    The model layer, generated as models.g.ts, contains a set of TypeScript interfaces that represent each client-exposed type in your data model. Each interface contains all the Properties of that type, as well as a $metadata property that references the metadata object for that type. Enums and Data Sources are also represented in the model layer.

    Read more about the Model layer

    API Client Layer

    The API client layer, generated as api-clients.g.ts, exports a class for each API controller that was generated for your data model. These classes are stateless and provide one method for each API endpoint. This includes both the standard set of endpoints created for Entity Models and Custom DTOs, as well as any custom Methods on the aforementioned types, as well as any methods on your Services.

    Read more about the API Client layer

    ViewModel Layer

    The ViewModel layer, generated as viewmodels.g.ts, exports a ViewModel class for each API-backed type in your data model (Entity Models, Custom DTOs, and Services). It also exports a ListViewModel type for Entity Models and Custom DTOs.

    These classes provide a wide array of functionality that is useful when interacting with your data model through a user interface. The generated ViewModels are the primary way that Coalesce is used when developing a Vue application.

    Read more about the ViewModel layer

    Vue Components

    open in new windowopen in new window

    The Vueopen in new window stack for Coalesce provides a set of components based on Vuetifyopen in new window, packaged up in an NPM package coalesce-vue-vuetify2open in new window or coalesce-vue-vuetify3open in new window. These components are driven primarily by the Metadata Layer, and include both low level input and display components like c-input and c-display that are highly reusable in the custom pages you'll build in your application, as well as high-level components like c-admin-table-page and c-admin-editor-page that constitute entire pages.

    Read more about the Vuetify Components here.

    Admin Views

    The Vue.js stack for Coalesce provides some high level components that provide functionality of whole pages like c-admin-table-page and c-admin-editor-page.

    The template described in Getting Started with Vue comes with routes already in place for these page-level components. For example, /admin/Person for a table, /admin/Person/edit to create a new Person, and /admin/Person/edit/:id to edit a Person.

    + + + diff --git a/stacks/vue/vue2-to-vue3.html b/stacks/vue/vue2-to-vue3.html new file mode 100644 index 000000000..2f029d99f --- /dev/null +++ b/stacks/vue/vue2-to-vue3.html @@ -0,0 +1,236 @@ + + + + + + + + + Vue 2 to Vue 3 | Coalesce + + + + +

    Vue 2 to Vue 3

    If you're already experienced with Vue 2 but are new to Vue 3, or if you're migrating an existing Vue 2 app to Vue 3, you should first read through the official migration guideopen in new window.

    Vuetify also offers a migration guideopen in new window to upgrade from Vuetify 2 to Vuetify 3.

    If you're new to Vue entirely, check out the rest of Vue docs and pick your learning pathopen in new window.

    Coalesce Upgrade Steps

    The changes specific to Coalesce when migrating from Vue2 to Vue3 are pretty minimal. Most of your work will be in following the Vue 3 Migration Guideopen in new window and the Vuetify 3 Migration Guideopen in new window.

    The table below contains the Coalesce-specific changes when migrating to Vue 3. However, the easiest migration path may be to disregard the table below and instead, instantiate the Coalesce Vue template or look at it on GitHubopen in new window and compare individual files between your project and the template side by side and ingest the changes that you observe.

    LocationOld (Vue 2)New (Vue 3)

    package.json

    {
    +  "dependencies": {
    +    "coalesce-vue-vuetify2": "x"
    +  }
    +}
    +
    {
    +  "dependencies": {
    +    "coalesce-vue-vuetify3": "x"
    +  }
    +}
    +

    vite.config.ts

    import { CoalesceVuetifyResolver } from "coalesce-vue-vuetify2/lib/build"
    +
    import { CoalesceVuetifyResolver } from "coalesce-vue-vuetify3/build"
    +
    +// Custom SASS options and `optimizeDeps` configuration can be removed
    +// since Vuetify3 no longer uses deprecated sass features,
    +// and pre-bundling styles no longer has appreciable benefit.
    +

    main.ts

    import "coalesce-vue-vuetify2/dist/coalesce-vue-vuetify.css"
    +
    +// Either of these:
    +import CoalesceVuetify from 'coalesce-vue-vuetify2/lib'
    +import CoalesceVuetify from 'coalesce-vue-vuetify2'
    +
    +Vue.use(CoalesceVuetify, {
    +  metadata: $metadata,
    +});
    +
    +
    import "coalesce-vue-vuetify3/styles.css"
    +
    +
    +import { createCoalesceVuetify } from "coalesce-vue-vuetify3";
    +
    +
    +const coalesceVuetify = createCoalesceVuetify({
    +  metadata: $metadata,
    +});
    +app.use(coalesceVuetify);
    +

    router.ts

    // Either of these:
    +import { CAdminTablePage, CAdminEditorPage } from 'coalesce-vue-vuetify2/lib';
    +import { CAdminTablePage, CAdminEditorPage } from 'coalesce-vue-vuetify2';
    +
    
    +import { CAdminEditorPage, CAdminTablePage } from "coalesce-vue-vuetify3";
    +

    Vitest/Jest tests

    If you had a global test setup file performing Vue configuration, you can likely remove it entirely, or at least remove the parts that configure Vue. Vue3 does not operate on global configuration like Vue2 did.

    See test-utils.tsopen in new window and HelloWorld.spec.tsopen in new window in the template for examples of Vue3 component testing.

    From Class Components to <script setup>

    The components in the Coalesce template for Vue 3 have switched from vue-class-component to Vue Composition API with <script setup>, the official recommendationopen in new window for building full Vue 3 applications.

    If you're used to writing components in Vue 2 with vue-class-component and vue-property-decorator, you can use this table of comparisons as a quick reference of what the equivalent features are using <script setup>open in new window and Vue Composition APIopen in new window. That said, this is not a replacement for learning and understanding the composition API. You should read the Composition API FAQopen in new window as well as the Reactivity Fundamentalsopen in new window documentation (make sure to set the API preference in the top left to Composition!).

    If you'd like to continue using class components with Vue 3 (e.g. upgrading an existing project where rewriting all components is not feasible), you can try switching to vue-facing-decoratoropen in new window.

    Note

    The examples below assume that unplugin-auto-import is being used (included in the Coalesce Vue3 template), eliminating the need to manually import common Vue Composition API functions.

    FeatureClass ComponentScript Setup

    Coalesce ViewModel and ListViewModel usage

    <script lang="ts">
    +import { Vue, Component } from "vue-property-decorator";
    +import { PersonViewModel, PersonListViewModel } from "@/viewmodels.g";
    +
    +@Component({})
    +export default class MyComponent extends Vue {
    +  public person = new PersonViewModel();
    +  public list = new PersonListViewModel();
    +
    +  async created() {
    +    await person.$load();
    +    await list.$load();
    +
    +    person.$startAutoSave(this);
    +    list.$startAutoLoad(this);
    +  }
    +}
    +</script>
    +
    <script lang="ts" setup>
    +import { PersonViewModel, PersonListViewModel } from "@/viewmodels.g";
    +
    +const person = new PersonViewModel();
    +const list = new PersonListViewModel();
    +
    +person.$useAutoSave();
    +list.$useAutoLoad();
    +
    +// If you need to await an async operation during component creation, 
    +// use an IIFE so that the component mount is not delayed.
    +(async function created() {
    +  await person.$load();
    +  await list.$load();
    +})();
    +</script>
    +

    @Prop, @Watch

    <script lang="ts">
    +import { Vue, Component, Prop, Watch } from "vue-property-decorator";
    +
    +@Component({})
    +export default class MyComponent extends Vue {
    +  @Prop({ default: "Student" })
    +  label!: string;
    +
    +  @Prop({ required: true })
    +  student!: ApplicationUserViewModel;
    +
    +  @Watch("label")
    +  labelChanged(newVal, oldVal) {
    +    console.log(`label changed. new:${newVal}, old:${oldVal}`)
    +  }
    +}
    +</script>
    +
    <script lang="ts" setup>
    +const props = defineProps({
    +  label: { type: String, default: "Student" },
    +  student: { type: Object as PropType<ApplicationUserViewModel>, required: true },
    +});
    +
    +watch(
    +  () => props.label,
    +  (newVal, oldVal) => {
    +    console.log(`label changed. new:${newVal}, old:${oldVal}`);
    +  }
    +);
    +</script>
    +

    Alternatively, props can be declared with type-only syntax, which is more concise but is a little more clunky when props have default values:

    <script lang="ts" setup>
    +const props = withDefaults(defineProps<{
    +  label?: string,
    +  student?: ApplicationUserViewModel
    +}>(), { label: 'Student' })
    +
    +// ... rest of the component
    +</script>
    +

    Reactive data

    <script lang="ts">
    +import { Vue, Component } from "vue-property-decorator";
    +import { PersonViewModel } from "@/viewmodels.g";
    +
    +@Component({})
    +export default class MyComponent extends Vue {
    +  public person = new PersonViewModel();
    +
    +  public checked = false;
    +
    +  public items = [
    +    { name: "Foo", checked: false, }
    +    { name: "Bar", checked: true, }
    +  ]
    +}
    +</script>
    +
    <script lang="ts" setup>
    +import { PersonViewModel } from "@/viewmodels.g";
    +
    +// Properties on coalesce-generated ViewModels have built in reactivity 
    +// and don't need to be wrapped ref/reactive unless you're going to replace 
    +// the entire top level object with a different instance.
    +const person = new PersonViewModel();
    +
    +const checked = ref(false);
    +
    +const items = reactive([
    +  { name: "Foo", checked: false, }
    +  { name: "Foo", checked: true, }
    +])
    +</script>
    +

    Computed values

    <script lang="ts">
    +import { Vue, Component } from "vue-property-decorator";
    +import { PersonViewModel } from "@/viewmodels.g";
    +
    +@Component({})
    +export default class MyComponent extends Vue {
    +  public person = new PersonViewModel()
    +
    +  get fullName() {
    +    return `${person.firstName} ${person.lastName}`
    +  }
    +}
    +</script>
    +
    <script lang="ts" setup>
    +import { PersonViewModel } from "@/viewmodels.g";
    +
    +const person = new PersonViewModel();
    +
    +const fullName = computed(() => `${person.firstName} ${person.lastName}`)
    +</script>
    +

    $emit, methods

    <template>
    +  <input
    +    :value="value"
    +    @input="inputChanged($event.target.value)"
    +  />
    +</template>
    +
    +<script lang="ts">
    +import { Vue, Component } from "vue-property-decorator";
    +
    +@Component({})
    +export default class MyComponent extends Vue {
    +  @Prop()
    +  value!: string;
    +
    +  inputChanged(v: string) {
    +    this.$emit('update:input', v)
    +  }
    +}
    +</script>
    +
    <template>
    +  <input
    +    :value="modelValue"
    +    @input="inputChanged(($event.target as HTMLInputElement).value)"
    +  />
    +</template>
    +
    +<script lang="ts" setup>
    +defineProps<{ modelValue: string | null }>();
    +
    +// This may seem tedious, but it enables full Typescript intellisense!
    +const emit = defineEmits<{
    +  (e: "update:modelValue", value: string | null): void;
    +}>();
    +
    +function inputChanged(v: string) {
    +  emit('update:modelValue', v)
    +}
    +</script>
    +
    + + + diff --git a/topics/coalesce-json.html b/topics/coalesce-json.html new file mode 100644 index 000000000..860941cf3 --- /dev/null +++ b/topics/coalesce-json.html @@ -0,0 +1,102 @@ + + + + + + + + + Code Generation Configuration | Coalesce + + + + +

    Code Generation Configuration

    In Coalesce, all configuration of the code generation is done in a JSON file. This file is typically named coalesce.json and is typically placed in the solution root.

    File Resolution

    When the code generation is run by invoking dotnet coalesce, Coalesce will try to find a configuration file via the following means:

    1. If an argument is specified on the command line, it will be used as the location of the file. E.g. dotnet coalesce C:/Projects/MyProject/config.json
    2. If no argument is given, Coalesce will try to use a file in the working directory named coalesce.json
    3. If no file is found in the working directory, Coalesce will crawl up the directory tree from the working directory until a file named coalesce.json is found. If such a file is never found, an error will be thrown.

    Contents

    A full example of a coalesce.json file, along with an explanation of each property, is as follows:

    {
    +    "webProject": {
    +        // Required: Path to the csproj of the web project. Path is relative to location of this coalesce.json file.
    +        "projectFile": "src/Coalesce.Web/Coalesce.Web.csproj",
    +
    +        // Optional: Framework to use when evaluating & building dependencies.
    +        // Not needed if your project only specifies a single framework - only required for multi-targeting projects.
    +        "framework": "netcoreapp2.0",
    +
    +        // Optional: Build configuration to use when evaluating & building dependencies.
    +        // Defaults to "Debug".
    +        "configuration": "Debug",
    +
    +        // Optional: Override the namespace prefix for generated C# code.
    +        // Defaults to MSBuild's `$(RootNamespace)` for the project.
    +        "rootNamespace": "MyCompany.Coalesce.Web",
    +    },
    +
    +    "dataProject": {
    +        // Required: Path to the csproj of the data project. Path is relative to location of this coalesce.json file.
    +        "projectFile": "src/Coalesce.Domain/Coalesce.Domain.csproj",
    +
    +        // Optional: Framework to use when evaluating & building dependencies.
    +        // Not needed if your project only specifies a single framework - only required for multi-targeting projects.
    +        "framework": "netstandard2.0",
    +
    +        // Optional: Build configuration to use when evaluating & building dependencies.
    +        // Defaults to "Release".
    +        "configuration": "Debug",
    +    },
    +
    +    // The name of the root generator to use. Defaults to "Knockout".
    +    // Available values are "Vue" and "Knockout".
    +    "rootGenerator": "Vue",
    +            
    +    // If set, specifies a list of whitelisted root type names that will restrict
    +    // which types Coalesce will use for code generation. 
    +    // Root types are those that must be annotated with [Coalesce].
    +    // Useful if want to segment a single data project into multiple web projects, 
    +    // or into different areas/directories within a single web project.
    +    "rootTypesWhitelist": [
    +        "MyDbContext", "MyCustomDto"
    +    ],
    +
    +    "generatorConfig": {
    +        // A set of objects keyed by generator name.
    +        // Generator names may optionally be qualified by their full namespace.
    +        // All generators are listed when running 'dotnet coalesce' with '--verbosity debug'.
    +        // For example, "Views" or "IntelliTect.Coalesce.CodeGeneration.Knockout.Generators.Views".
    +        "GeneratorName": {
    +            // Optional: true if the generator should be disabled.
    +            "disabled": true,
    +            // Optional: Configures a path relative to the default output path for the generator
    +            // where that generator's output should be placed instead.
    +            "targetDirectory": "../DifferentFolder"
    +        },
    +        // Indentation for generated C# is configurable by type (API controllers, DTO classes and regular View controllers)
    +        // It defaults to 4 spaces
    +        "ApiController": {
    +            "indentationSize": 2 
    +        },
    +        "ClassDto": {
    +            "indentationSize": 2 
    +        },
    +        "ViewController" : {
    +            "indentationSize": 2
    +        }
    +    }
    +}
    +

    Additional CLI Options

    There are a couple of extra options which are only available as CLI parameters to dotnet coalesce. These options do not affect the behavior of the code generation - only the behavior of the CLI itself.

    --debug

    When this flag is specified when running dotnet coalesce, Coalesce will wait up to 60 seconds for a debugger to be attached to its process before starting code generation.

    -v|--verbosity <level>

    Set the verbosity of the output. Options are trace, debug, information, warning, error, critical, and none.

    + + + diff --git a/topics/security.html b/topics/security.html new file mode 100644 index 000000000..6dcb5c912 --- /dev/null +++ b/topics/security.html @@ -0,0 +1,308 @@ + + + + + + + + + Security | Coalesce + + + + +

    Security

    This page is a comprehensive overview of all the techniques that can be used in a Coalesce application to restrict the usage of API endpoints that Coalesce generates.

    Endpoint Security

    Coalesce generates API endpoints by traversing your data model's classes, starting from types annotated with [Coalesce]. This usually includes your DbContext class, as well as any Service classes or interfaces.

    Classes can be hidden from Coalesce entirely by annotating them with [InternalUse], preventing generation of API endpoints for that class, as well as preventing properties of that type from being exposed.

    DbSet<> properties on your DbContext class can also be annotated with [InternalUse], causing that type to be treated by Coalesce like an External Type rather than an Entity, once again preventing generation of API endpoints but without preventing properties of that type from being exposed.

    Standard CRUD Endpoints

    For each of your Entities and Custom DTOs, Coalesce generates a set of CRUD API endpoints (/get, /list, /count, /save, /bulkSave, and /delete).

    The default behavior is that all endpoints require an authenticated user (anonymous users are rejected).

    These endpoints can be secured by placing any or all of the [Read], [Create], [Edit], and [Delete] attributes on the the class. Each attribute can specify required roles for that action, or open that action to anonymous, unauthenticated users, or disable the endpoint entirely.

    This security is applied to the generated controllersopen in new window. The [Read] attribute on a class does not affect instances of that class when those instances are present as child properties of other types, since in those scenarios the data will be coming from a different endpoint on a different controller.

    EndpointsGoverning Attributes

    /get, /list, /count, /bulkSave

    [ReadAttribute]
    +

    Note: the root model for a bulk save operation requires read permission. All other entities affected by the bulk save operation require their respective attribute for Create/Edit/Delete.

    /save

    [CreateAttribute] // Affects saves of new entities
    +[EditAttribute]   // Affects saves of existing entities
    +

    /delete

    [DeleteAttribute]
    +

    Here are some examples of applying security attributes to an entity class. If a particular action doesn't need to be restricted, you can omit that attribute, but this example shows usages of all four:

    // Allow read access by unauthenticated, anonymous users:
    +[Read(SecurityPermissionLevels.AllowAll)]
    +// Allow creation of new entities by the Admin and HR roles (params string[] style):
    +[Create("Admin", "HR")]
    +// Allow editing of existing Employee entities by users with the Admin or HR roles (CSV style):
    +[Edit("Admin,HR")]
    +// Prohibit deletion of Employee entities
    +[Delete(SecurityPermissionLevels.DenyAll)]
    +public class Employee 
    +{
    +    public int EmployeeId { get; set; }
    +}
    +

    Custom Methods and Services

    To secure the endpoints generated for your Custom Methods and Services, the [Execute] attribute can be used to specify a set of required roles for that endpoint, or to open that endpoint to anonymous users.

    The default behavior is that all endpoints require an authenticated user (anonymous users are rejected).

    For example:

    public class Employee 
    +{
    +    public int EmployeeId { get; set; }
    +
    +    [Coalesce, Execute("Payroll,HR")]
    +    public void GiveRaise(int centsPerHour) {
    +        // Only Payroll and HR users can call this method
    +    }
    +
    +    [Coalesce, Execute(SecurityPermissionLevels.AllowAll)]
    +    public void SendMessage(string message) {
    +        // Anyone (even anonymous, unauthenticated users) can call this method.
    +    }
    +}
    +

    Property/Column Security

    Internal Properties

    Properties can be hidden from Coalesce entirely, either with the [InternalUse] attribute or non-public C# access modifiers.

    The properties in the following example are hidden entirely from all Coalesce functionality and generated APIs:

    using IntelliTect.Coalesce.DataAnnotations;
    +public class Employee 
    +{
    +  // InternalUseAttribute hides anything from Coalesce.
    +  [InternalUse]
    +  public string Name { get; set; }
    +
    +  // Non-public C# access modifiers will hide properties from Coalesce:
    +  internal decimal Salary { get; set; }
    +
    +  // Property's type is [InternalUse], so properties using that type are also internal.
    +  public Department Department { get; set; }
    +}
    +
    +[InternalUse]
    +public class Department
    +{
    +  // All properties on an [InternalUse] type are non-exposed,
    +  // since the parent type is not exposed.
    +  public string Name { get; set; }
    +}
    +

    Attributes

    The [Read] and [Edit] attributes can be placed on the properties on your Entities and External Types to apply role-based restrictions to the usage of that property.

    This security is primarily executed and enforced by the mapping that occurs in the generated DTOs. It is also checked by the Standard Data Source to prevent sorting, searching, and filtering by properties that a user is not permitted to read.

    Read-Only Properties

    A property in Coalesce can be made read-only in any of the following ways:

    using IntelliTect.Coalesce.DataAnnotations;
    +using System.ComponentModel;
    +public class Employee 
    +{
    +  // A property with a [Read] attribute but no [Edit] attribute is read-only:
    +  [Read]
    +  public string Name { get; set; }
    +
    +  // Payroll users and HR users can read this property. Nobody can edit it:
    +  [Read("Payroll,HR")]
    +  public decimal Salary { get; set; }
    +
    +  // Using System.ComponentModel.ReadOnlyAttribute:
    +  [ReadOnly(true)]
    +  public DateTime BirthDate { get; set; }
    +
    +  // Non-public setter:
    +  public DateTime StartDate { get; internal set; }
    +
    +  // Edits denied:
    +  [Edit(SecurityPermissionLevels.DenyAll)]
    +  public string EmployeeNumber { get; set; }
    +}
    +

    Read/Write Properties

    Reading and writing a property in Coalesce can be restricted by roles:

    using IntelliTect.Coalesce.DataAnnotations;
    +public class Employee 
    +{
    +  // A property with no attributes is readable and writable without restriction
    +  public string Name { get; set; }
    +
    +  // When a [Read] and [Edit] attributes are both present,
    +  // the read roles are required for edits in addition to any edit roles.
    +  // Property is only readable by Payroll & HR,
    +  // and is also only editable by Payroll & HR.
    +  [Read("Payroll,HR"), Edit]
    +  public DateTime BirthDate { get; set; }
    +
    +  // Property is readable by Payroll and HR, and editable only by Payroll.
    +  [Read("Payroll", "HR"), Edit("Payroll")]
    +  public decimal Salary { get; set; }
    +
    +  // Property is readable by Payroll, and editable only by a user who is both Payroll AND HR.
    +  [Read("Payroll"), Edit("HR")]
    +  public DateTime StartDate { get; set; }
    +
    +  // Init-only properties on entities can only be set by the first /save of the entity.
    +  public string EmployeeNumber { get; init; }
    +}
    +

    A few of the examples above point out that when a property is restricted for reading by roles, those roles are also required when editing that property. This is because it usually doesn't make sense for a user to change a value when they have no way of knowing what the original value was. If you have a situation where a property should be editable without knowing the original value, use a custom method on the model to accept and set the new value.

    Row-level Security

    Data Sources

    In Coalesce, Data Sources are the mechanism that you can extend to implement row-level security on your Entities and Custom DTOs.

    Data Sources are used when fetching results for /get, /list, and /count endpoints, and when fetching the target or result of a /save, /bulkSave, or /delete, and when fetching the invocation target of an Instance Method.

    By default, your entities will be fetched using the Standard Data Source, but you can declare a custom default data source for each of your entities to override this default functionality. The default functionality here includes the default loading behavior, a feature where the Standard Data Source automatically includes the immediate relationships of requested entities. This can be suppressed by overriding the GetQuery method on your custom data source and not calling the base method, or by placing [Read(NoAutoInclude = true)] on classes or navigation properties that you do not want automatically included.

    For most use cases, all your security rules will be implemented in the GetQuery/GetQueryAsync method. This is the most foundational method of the data source that all other functions in the data source build upon. Any predicates applied to the query of a type's default data source will affect all of the type's generated API endpoints (except for static custom methods).

    There are a few different techniques that you can use to apply filtering in a data source, each one working for a specific use case. The example below includes an example of each technique.

    Query Predicates

    The Query Predicates technique involves applying a .Where() predicate to your query to filter the root entities that are returned by the query using some database-executed logic. This is a form of row-level security and can be used to only include a record based on the values of that record in the database.

    Conditional Includes

    The Conditional Includes technique involves conditionally appending .Include() calls to your query only when some server-executed criteria is met. Usually this involves checking the roles of a user and only including a navigation property if the user is in the requisite role. This technique cannot be used with database-executed logic and is therefore behaves more like table-level security than row-level security.

    Filtered Includes

    The Filtered Includes technique involves using EF Core filtered includesopen in new window to apply database-executed logic to filter the rows of child collection navigation properties.

    EF filtered Includes cannot be used to apply database-executed filters to reference navigation properties due to lack of EF supportopen in new window - see the sections below on transform results and global query filters for two possible solutions.

    A complex example using all three of the above techniques:

    public class Employee 
    +{
    +  public int EmployeeId { get; set; }
    +  public bool IsIntern { get; set; }
    +  public List<DepartmentMember> DepartmentMembers { get; set; }
    +
    +  // Override the default data source for Employee with a custom one:
    +  [DefaultDataSource]
    +  public class DefaultSource : StandardDataSource<Employee, AppDbContext>
    +  {
    +    public DefaultSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Employee> GetQuery(IDataSourceParameters parameters) {
    +      IQueryable<Employee> query = Db.Employees;
    +
    +      // TECHNIQUE: Conditional Includes - subset child objects using server-executed logic:
    +      if (User.IsInRole("HR")) {
    +        // HR can see everything. Return early so they are not subjected to the other filters:
    +        return query.Include(e => e.DepartmentMembers).ThenInclude(dm => dm.Department);
    +      }
    +
    +      // TECHNIQUE: Query Predicates - subset root objects using database-executed logic:
    +      int employeeId = User.GetEmployeeId();
    +      query = query.Where(e => 
    +          // Anyone can see interns
    +          e.IsIntern ||
    +          // Otherwise, a user can only see employees in their own departments:
    +          e.DepartmentMembers.Any(dm => dm.Department.DepartmentMembers.Any(u => u.EmployeeId == employeeId))
    +        );
    +
    +      // TECHNIQUE: EF Core Filtered Includes - subset collections using database-executed logic.
    +      // Include the departments of employees, but only those that the current user is a member of.
    +      query = query.Include(e => e.DepartmentMembers
    +        .Where(dm => dm.Department.DepartmentMembers.Any(u => u.EmployeeId == employeeId)))
    +        .ThenInclude(dm => dm.Department);
    +      
    +      return query;
    +    }
    +  }
    +}
    +
    +public class Department 
    +{
    +  public int DepartmentId { get; set; }
    +  public string Name { get; set; }
    +  public List<DepartmentMember> DepartmentMembers { get; set; }
    +
    +  // Override the default data source for Department with a custom one:
    +  [DefaultDataSource]
    +  public class DefaultSource : StandardDataSource<Department, AppDbContext>
    +  {
    +    public DefaultSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Department> GetQuery(IDataSourceParameters parameters) {
    +      IQueryable<Department> query = Db.Departments
    +        .Include(e => e.DepartmentMembers).ThenInclude(dm => dm.Employee);
    +
    +      if (!User.IsInRole("HR")) 
    +      {
    +        // Non-HR users can only see their own departments:
    +        query = query.Where(d => d.DepartmentMembers.Any(dm => dm.EmployeeId == User.GetEmployeeId()));
    +      }
    +
    +      return query;
    +    }
    +  }
    +}
    +
    +// Only HR can directly read or modify DepartmentMember records.
    +[Read("HR"), Create("HR"), Edit("HR"), Delete("HR")]
    +public class DepartmentMember 
    +{
    +  public int Id { get; set; }
    +
    +  public int DepartmentId { get; set; }
    +  public Department Department { get; set; }
    +  public int EmployeeId { get; set; }
    +  public Employee Employee { get; set; }
    +}
    +
    +

    Transform Results

    There exists a fourth technique in Data Sources for applying filtered includes: the TransformResultsAsync method. Unlike the other techniques above that are performed in the GetQuery method and applied at the beginning of the data source query pipeline, TransformResults is applied at the very end of the process against the materialized results. It also only affects the responses from the generated /get, /list, /save, /bulkSave, and /delete endpoints - it has no bearing on the invocation target of instance methods.

    The primary purpose of TransformResults is to conditionally load navigation properties. This was very useful before EF Core introduced native filtered includes for collection navigation properties, and is still useful for applying filtered includes to reference navigation properties since EF does not support thisopen in new window. It can also be used for any kind of filtered includes if native EF filtered includes get translated into poorly-performant SQL, or it can be used to populate external type or other non-database-mapped properties on your entities.

    The general technique for using TransformResults involves using EF Core Explicit Loadingopen in new window to attach additional navigation properties to the result set, and then using Coalesce's .IncludedSeparately() method in the data source's GetQuery so that Coalesce can still build the correct Include Tree to shape the serialization of your results.

    public class Employee 
    +{
    +  public int EmployeeId { get; set; }
    +  public int ManagerId { get; set; }
    +  public Employee Manager { get; set; }
    +
    +  [DefaultDataSource]
    +  public class DefaultSource : StandardDataSource<Employee, AppDbContext>
    +  {
    +    public DefaultSource(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override IQueryable<Employee> GetQuery(IDataSourceParameters parameters) 
    +      // Use IncludedSeparately to instruct Coalesce that we're going to 
    +      // manually populate the Manager, and that it should be mapped to the result DTOs
    +      // despite not being eagerly loaded with EF's .Include() method.
    +      => Db.Employees.IncludedSeparately(e => e.Manager);
    +
    +    public override async Task TransformResultsAsync(
    +      IReadOnlyList<Employee> results,
    +      IDataSourceParameters parameters
    +    )
    +    {
    +      foreach (var employee in results)
    +      {
    +        // Only load the employee's manager if the current logged in user is that manager.
    +        if (employee.ManagerId == User.GetEmployeeId() && employee.Manager is null) {
    +          await Db.Employees.Where(e => e.EmployeeId == employee.ManagerId).LoadAsync();
    +        }
    +      }
    +    }
    +  }
    +}
    +

    Alternatively, and indeed preferably, you can often formulate a query that does not use iteration and requires only a single database round-trip:

    public override async Task TransformResultsAsync(
    +  IReadOnlyList<Employee> results,
    +  IDataSourceParameters parameters
    +)
    +{
    +  var managerIds = results.Select(e => e.ManagerId).ToList();
    +  await Db.Employees
    +    .Where(e => managerIds.Contains(e.ManagerId) && e.EmployeeId == User.GetEmployeeId())
    +    .LoadAsync();
    +}
    +

    For a more complete explanation of everything you can do with data sources, see the full Data Sources documentation page.

    EF Global Query Filters

    Since Coalesce's data access layer is built on top of Entity Framework, you can also use Entity Framework's Global Query Filtersopen in new window feature to apply row-level security.

    This approach is less flexible than custom Coalesce data sources and has other drawbacksopen in new window as well, but on the other hand it has more absolute authority, is less susceptible to issues like inadvertently returning data through unfiltered navigation properties, and can sometimes require less work to implement than individual data sources.

    Global Query Filters are also the only way to implement database-executed filtered includes of reference navigation propertiesopen in new window, as there is no version of .Include() for reference navigation properties that allows a database-executed predicate to be applied. See this open issueopen in new window on EF Core.

    Foreign Key Injection Vulnerabilities

    When a user is saving a model with Coalesce, they can provide values for the model's foreign key properties. When this interaction takes place through a user interface, the user is not likely to produce a foreign key referencing an object that the user is not allowed to view.

    A malicious user, however, is a different story. Imagine a user who is brute-forcing the /save endpoint on one of your entities, enumerating values of a foreign key. The may be trying to leak data through navigation property values returned by the response from the save, or they may be trying to inject their data into an object graph that they do not otherwise have access to.

    If this scenario sounds like a plausible threat vector your application, be sure to perform sufficient validation of incoming foreign keys to ensure that the user is allowed to use a particular foreign key value before saving it to your database.

    Also consider making any required foreign keys that should not change for the lifetime of an entity into init-only properties (i.e. use the init accessor in C# instead of the set accessor). While this does not entirely solve the foreign key injection issue, it eliminates the need to validate that a user is not changing the parent of an object if such an operation is not desirable.

    Server-side Data Validation

    Coalesce, as of version 4, will by default perform server-side validation of incoming data using validation attributes.

    Your database will also enforce any constraints (referential integrity, not null, check constraints, etc.), but errors produced by your database will manifest as exceptions, which are not user-friendly.

    For any custom validation that cannot be implemented by attributes, you must implement that yourself for saves and deletes or custom methods.

    Attribute Validation

    Historically, Coalesce did not provide any automatic, attribute-based validation of incoming data. As of Coalesce 4.0, automatic server side validation using ValidationAttributeopen in new window-derived attributes on your models is enabled by default.

    In addition to any validation attributes present on your model properties and method parameters, there are some other rules that work similarly to the default validation in ASP.NET Core:

    • The C# 11 required keyword also acts like a RequiredAttribute
    • If C# nullable reference types are enabled, non-nullable reference types are required required.
    • Non-nullable value types are implicitly optional, with the exception of non-nullable foreign keys, which are required.

    To disable this functionality for your entire application, disable the corresponding configuration options on CoalesceOptions. For example, in Startup.cs or Program.cs:

    services.AddCoalesce<AppDbContext>(b => b.Configure(o =>
    +{
    +    // Set either to false to disable:
    +    o.ValidateAttributesForSaves = true;
    +    o.ValidateAttributesForMethods = true;
    +}));
    +

    Each option also has a more granular override:

    ValidateAttributesForSaves

    Enabling ValidateAttributesForSaves causes the Standard Behaviors to perform validation of validation attributes during /save or /bulkSave calls, preventing a save when validation fails.

    This can be overridden per type or even per request by setting the ValidateAttributesForSaves property on a custom Behaviors instance.

    ValidateAttributesForMethods

    Enabling ValidateAttributesForMethods causes the generated controllers for custom methods to perform validation of incoming parameters. Validation attributes may be placed on method parameters, and validation will also be performed against the members of any complex type parameters.

    This can be overridden per method by setting the ValidateAttributes property on ExecuteAttribute for the method.

    Saves and Deletes

    Validation of /save, /bulkSave, and /delete actions against Entities and Custom DTOs are performed by the Behaviors for the type. Automatic attribute based validation can be used (saves only), or Behaviors can be overridden to perform validation and other customization of the save and delete process, as in the following example:

    public class Employee 
    +{
    +  public int IsCeo { get; set; }
    +  public decimal Salary { get; set; }
    +
    +  [Coalesce]
    +  public class Behaviors : StandardBehaviors<Employee, AppDbContext>
    +  {
    +    public Behaviors(CrudContext<AppDbContext> context) : base(context) { }
    +
    +    public override ItemResult BeforeSave(SaveKind kind, Employee? oldItem, Employee item)
    +    {
    +      // `oldItem` is a shallow copy of entity from the database,
    +      // and `item` is the tracked entity with incoming user data applied to it.
    +      if (item.Salary > 1_000_000m && !oldItem.IsCeo) return "Salary is too high.";
    +      return true;
    +    }
    +
    +    public override ItemResult BeforeDelete(Case item)
    +    {
    +      if (item.IsCeo) return "The CEO cannot be fired.";
    +      return true;
    +    }
    +  }
    +}
    +

    Custom Methods and Services

    For Custom Methods and Services, you can perform your own custom validation and return errors when validation fails. You can also use attribute based validation. Custom methods that need to return errors to the client are recommended to wrap their return type in an ItemResult<T>, allowing errors to be received and handled elegantly by your Coalesce Typescript code.

    public class Employee 
    +{
    +  public decimal Salary { get; set; }
    +
    +  [Coalesce]
    +  public ItemResult<decimal> GiveRaise(decimal raiseAmount)
    +  {
    +    if (raiseAmount > 3.5m) return "Raises must be less than $3.50."
    +    Salary += raiseAmount;
    +    return Salary;
    +  }
    +}
    +

    Security Overview Page

    Coalesce provides batteries-included page that you can view to review the effective security rules in place for all the Coalesce-generated code in your project. Add this page to your application by mapping it as a route, either directly on WebHost in .NET 6+, or in UseEndpoints for 3.1+.

    TIP

    If you include the security overview in your production app, you should secure it with an authorization policy like in the example below. Alternatively, only map the endpoint in non-production environments.

    // .NET 6+ Program.cs:
    +app.MapCoalesceSecurityOverview("coalesce-security").RequireAuthorization(
    +    new AuthorizeAttribute { Roles = env.IsDevelopment() ? null : "Admin" }
    +);
    +
    +// .NET Core 3.1+ Startup.cs:
    +app.UseEndpoints(endpoints =>
    +{
    +    endpoints.MapCoalesceSecurityOverview("coalesce-security").RequireAuthorization(
    +        new AuthorizeAttribute { Roles = env.IsDevelopment() ? null : "Admin" }
    +    );
    +});
    +

    Example of the contents of the security overview page:

    Testing Your Security

    If your application has complex security requirements and/or sensitive data that needs to be protected, you are encouraged to invest time into creating a set of automated tests to ensure that it is working how you expect.

    The most comprehensive way to do this is to build a suite of integration tests using Microsoft's in-memory test server infrastructureopen in new window. Follow Microsoft's documentation to set up a test project, and then write tests against your API endpoints. You will want to substitute your Entity Framework database provideropen in new window with an in-memory Sqlite instance, and add a mock authentication handleropen in new window to simulate authentication (we're mainly focused on testing authorization, not authentication).

    + + + diff --git a/topics/startup.html b/topics/startup.html new file mode 100644 index 000000000..1aec952d1 --- /dev/null +++ b/topics/startup.html @@ -0,0 +1,62 @@ + + + + + + + + + Application Configuration | Coalesce + + + + +

    Application Configuration

    In order for Coalesce to work in your application, you must register the needed services in your Startup.cs file. Doing so is simple:

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddCoalesce<AppDbContext>();
    +    ...
    +}
    +

    This registers all the basic services that Coalesce needs in order to work with your EF DbContext. However, there are many more options available. Here's a more complete invocation of AddCoalesce that takes advantage of many of the options available:

    public void ConfigureServices(IServiceCollection services)
    +{
    +    services.AddCoalesce(builder => builder
    +        .AddContext<AppDbContext>()
    +        .UseDefaultDataSource(typeof(MyDataSource<,>))
    +        .UseDefaultBehaviors(typeof(MyBehaviors<,>))
    +        .UseTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"))
    +        .Configure(o =>
    +        {
    +            o.ValidateAttributesForMethods = true; // note: true is the default
    +            o.ValidateAttributesForSaves = true; // note: true is the default
    +            o.DetailedExceptionMessages = true;
    +            o.ExceptionResponseFactory = ctx =>
    +            {
    +                if (ctx.Exception is FileNotFoundException)
    +                {
    +                    ctx.HttpContext.Response.StatusCode = 404; // Optional - set a specific response code.
    +                    return new IntelliTect.Coalesce.Models.ApiResult(false, "File not found");
    +                }
    +                return null;
    +            };
    +        });
    +    );
    +}
    +

    A summary is as follows:

    .AddContext<AppDbContext>()

    Register services needed by Coalesce to use the specified context. This is done automatically when calling the services.AddCoalesce<AppDbContext>(); overload.

    .UseDefaultDataSource(typeof(MyDataSource<,>))

    Overrides the default data source used, replacing the Standard Data Source. See Data Sources for more details.

    .UseDefaultBehaviors(typeof(MyBehaviors<,>))

    Overrides the default behaviors used, replacing the Standard Behaviors. See Behaviors for more details.

    .UseTimeZone(TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"))

    Specify a static time zone that should be used when Coalesce is performing operations on dates/times that lack timezone information. For example, when a user inputs a search term that contains only a date, Coalesce needs to know what timezone's midnight to use when performing the search.

    .UseTimeZone<ITimeZoneResolver>()

    Specify a service implementation to use to resolve the current timezone. This should be a scoped service, and will be automatically registered if it is not already. This allows retrieving timezone information on a per-request basis from HTTP headers, Cookies, or any other source.

    .Configure(...)

    Configure additional options for Coalesce runtime behavior. Current options include options for server-side validation, and options for exception handling. See individual members for details.

    + + +