diff --git a/404.html b/404.html index 889246688..60a74d66f 100644 --- a/404.html +++ b/404.html @@ -3,16 +3,16 @@ -Booster Framework +Booster Framework - - - + + +
Skip to main content

Page Not Found

We could not find what you were looking for.

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

diff --git a/Home/index.html b/Home/index.html index 3d3f2fb77..93dce047a 100644 --- a/Home/index.html +++ b/Home/index.html @@ -3,16 +3,16 @@ -Booster Framework +Booster Framework - - - + + +
Skip to main content
Booster Logo

Build serverless event-sourcing microservices in minutes instead of months!

Booster is an open-source minimalistic TypeScript framework to build event-sourced services with the minimal amount of code possible, but don't let its innocent appearance fool you; Booster analyzes the semantics of your code, sets up theoptimal infrastructure to run your application at scale, and even generates a fully-working GraphQL API for you – don't even mind about writing the resolvers or maintaining your GraphQL schema, it will do that for you too.

And have we mentioned it's all open-source and free? But not free like you have a few build minutes per month or anything like that, we mean, real free. Everything remains between you, your CI/CD scripts (wherever you want to put them), and your own cloud accounts. Nothing is hidden under the carpet, you can visit the Github repository and see every single detail.

diff --git a/architecture/command/index.html b/architecture/command/index.html index 6c18656c6..e5f2a8a5f 100644 --- a/architecture/command/index.html +++ b/architecture/command/index.html @@ -3,16 +3,16 @@ -Command | Booster Framework +Command | Booster Framework - - - + + +
Skip to main content

Command

@@ -78,6 +78,6 @@

C
  • UpdateCartShippingAddress
  • Despite you can place commands, and other Booster files, in any directory, we strongly recommend you to put them in <project-root>/src/commands. Having all the commands in one place will help you to understand your application's capabilities at a glance.

    -
    <project-root>
    ├── src
    │   ├── commands <------ put them here
    │   ├── common
    │   ├── config
    │   ├── entities
    │   ├── events
    │   ├── index.ts
    │   └── read-models

    +
    <project-root>
    ├── src
    │   ├── commands <------ put them here
    │   ├── common
    │   ├── config
    │   ├── entities
    │   ├── events
    │   ├── index.ts
    │   └── read-models
    \ No newline at end of file diff --git a/architecture/entity/index.html b/architecture/entity/index.html index 1101cd595..5befc6820 100644 --- a/architecture/entity/index.html +++ b/architecture/entity/index.html @@ -3,16 +3,16 @@ -Entity | Booster Framework +Entity | Booster Framework - - - + + +
    Skip to main content

    Entity

    @@ -59,6 +59,6 @@

    E
  • Stock
  • Entities live within the entities directory of the project source: <project-root>/src/entities.

    -
    <project-root>
    ├── src
    │ ├── commands
    │ ├── common
    │ ├── config
    │ ├── entities <------ put them here
    │ ├── events
    │ ├── index.ts
    │ └── read-models

    +
    <project-root>
    ├── src
    │ ├── commands
    │ ├── common
    │ ├── config
    │ ├── entities <------ put them here
    │ ├── events
    │ ├── index.ts
    │ └── read-models
    \ No newline at end of file diff --git a/architecture/event-driven/index.html b/architecture/event-driven/index.html index d5bf12665..9cf362ff3 100644 --- a/architecture/event-driven/index.html +++ b/architecture/event-driven/index.html @@ -3,16 +3,16 @@ -Booster architecture | Booster Framework +Booster architecture | Booster Framework - - - + + +
    Skip to main content

    Booster architecture

    @@ -23,6 +23,6 @@

    Booster applications are event-driven and event-sourced so, the source of truth is the whole history of events. When a client submits a command, Booster wakes up and handles it throght Command Handlers. As part of the process, some Events may be registered as needed.

    On the other side, the framework caches the current state by automatically reducing all the registered events into Entities. You can also react to events via Event Handlers, triggering side effect actions to certain events. Finally, Entities are not directly exposed, they are transformed or projected into ReadModels, which are exposed to the public.

    In this chapter you'll walk through these concepts in detail.

    -
    +

    📄️ Command

    Commands are any action a user performs on your application. For example, RemoveItemFromCart, RatePhoto or AddCommentToPost. They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a request on a REST API. Command issuers can also send data on a command as parameters.

    📄️ Event

    An event is a fact of something that has happened in your application. Every action that takes place on your application should be stored as an event. They are stored in a single collection, forming a set of immutable records of facts that contain the whole story of your application. This collection of events is commonly known as the Event Store.

    📄️ Event handler

    Learn how to react to events and trigger side effects in Booster by defining event handlers.

    📄️ Entity

    If events are the source of truth of your application, entities are the current state of your application. For example, if you have an application that allows users to create bank accounts, the events would be something like AccountCreated, MoneyDeposited, MoneyWithdrawn, etc. But the entities would be the BankAccount themselves, with the current balance, owner, etc.

    📄️ Read model

    A read model contains the data of your application that is exposed to the client through the GraphQL API. It's a projection of one or more entities, so you dont have to directly expose them to the client. Booster generates the GraphQL queries that allow you to fetch your read models.

    📄️ Notifications

    Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators.

    📄️ Queries

    ReadModels offer read operations over reduced events. On the other hand, Queries provide a way to do custom read operations.

    \ No newline at end of file diff --git a/architecture/event-handler/index.html b/architecture/event-handler/index.html index e8228c319..7ad08594a 100644 --- a/architecture/event-handler/index.html +++ b/architecture/event-handler/index.html @@ -3,16 +3,16 @@ -Event handler | Booster Framework +Event handler | Booster Framework - - - + + +
    Skip to main content

    Event handler

    @@ -36,6 +36,6 @@

    Reading entities from event handlers

    There are cases where you need to read an entity to make a decision based on its current state. Different side effects can be triggered depending on the current state of the entity. Given the previous example, if a user does not want to receive emails when a product is out of stock, we should be able check the user preferences before sending the email.

    For that reason, Booster provides the Booster.entity function. This function allows you to retrieve the current state of an entity. Let's say that we want to check the status of a product before we trigger its availability update. In that case we would call the Booster.entity function, which will return information about the entity.

    -
    src/event-handlers/handle-availability.ts
    @EventHandler(StockMoved)
    export class HandleAvailability {
    public static async handle(event: StockMoved, register: Register): Promise<void> {
    const product = await Booster.entity(Product, event.productID)
    if (product.stock < 0) {
    register.events([new ProductOutOfStock(event.productID)])
    }
    }
    }
    +
    src/event-handlers/handle-availability.ts
    @EventHandler(StockMoved)
    export class HandleAvailability {
    public static async handle(event: StockMoved, register: Register): Promise<void> {
    const product = await Booster.entity(Product, event.productID)
    if (product.stock < 0) {
    register.events([new ProductOutOfStock(event.productID)])
    }
    }
    }
    \ No newline at end of file diff --git a/architecture/event/index.html b/architecture/event/index.html index 410c53c21..5ed3423ff 100644 --- a/architecture/event/index.html +++ b/architecture/event/index.html @@ -3,16 +3,16 @@ -Event | Booster Framework +Event | Booster Framework - - - + + +
    Skip to main content
    +
    <project-root>
    ├── src
    │ ├── commands
    │ ├── common
    │ ├── config
    │ ├── entities
    │ ├── events <------ put them here
    │ ├── index.ts
    │ └── read-models
    \ No newline at end of file diff --git a/architecture/notifications/index.html b/architecture/notifications/index.html index f524226fb..5f4331927 100644 --- a/architecture/notifications/index.html +++ b/architecture/notifications/index.html @@ -3,16 +3,16 @@ -Notifications | Booster Framework +Notifications | Booster Framework - - - + + +
    Skip to main content

    Notifications

    @@ -32,6 +32,6 @@

    In this example, each CartAbandoned notification will have its own partition key, which is specified in the constructor as the field key, it can be called in any way you want. This will allow for parallel processing of notifications, making the system more performant.

    Reacting to notifications

    Just like events, notifications can be handled by event handlers in order to trigger other processes. Event handlers are responsible for listening to events and notifications, and then performing specific actions in response to them.

    -

    In conclusion, defining notifications in the Booster Framework is a simple and straightforward process that can be done using the @Notification and @partitionKey decorators.

    +

    In conclusion, defining notifications in the Booster Framework is a simple and straightforward process that can be done using the @Notification and @partitionKey decorators.

    \ No newline at end of file diff --git a/architecture/queries/index.html b/architecture/queries/index.html index 2d18241e8..fce16c1b9 100644 --- a/architecture/queries/index.html +++ b/architecture/queries/index.html @@ -3,16 +3,16 @@ -Queries | Booster Framework +Queries | Booster Framework - - - + + +
    Skip to main content
    +
    \ No newline at end of file diff --git a/architecture/read-model/index.html b/architecture/read-model/index.html index b8b88dcd1..34766392c 100644 --- a/architecture/read-model/index.html +++ b/architecture/read-model/index.html @@ -3,16 +3,16 @@ -Read model | Booster Framework +Read model | Booster Framework - - - + + +
    Skip to main content

    Read model

    @@ -93,6 +93,6 @@

    Quer

    Read models naming convention

    As it has been previously commented, semantics plays an important role in designing a coherent system and your application should reflect your domain concepts, we recommend choosing a representative domain name and use the ReadModel suffix in your read models name.

    Despite you can place your read models in any directory, we strongly recommend you to put them in <project-root>/src/read-models. Having all the read models in one place will help you to understand your application's capabilities at a glance.

    -
    <project-root>
    ├── src
    │   ├── commands
    │   ├── common
    │   ├── config
    │   ├── entities
    │   ├── read-models <------ put them here
    │   ├── events
    │   ├── index.ts
    │   └── read-models
    +
    <project-root>
    ├── src
    │   ├── commands
    │   ├── common
    │   ├── config
    │   ├── entities
    │   ├── read-models <------ put them here
    │   ├── events
    │   ├── index.ts
    │   └── read-models
    \ No newline at end of file diff --git a/assets/css/styles.2aa7771b.css b/assets/css/styles.8e261003.css similarity index 66% rename from assets/css/styles.2aa7771b.css rename to assets/css/styles.8e261003.css index 21ec13d47..135389b60 100644 --- a/assets/css/styles.2aa7771b.css +++ b/assets/css/styles.8e261003.css @@ -1 +1 @@ -@import url(https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap);.col,.container{padding:0 var(--ifm-spacing-horizontal);width:100%}.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{margin-bottom:calc(var(--ifm-heading-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown li,body{word-wrap:break-word}body,ol ol,ol ul,ul ol,ul ul{margin:0}pre,table{overflow:auto}blockquote,pre{margin:0 0 var(--ifm-spacing-vertical)}.breadcrumbs__link,.button{transition-timing-function:var(--ifm-transition-timing-default)}.button,code{vertical-align:middle}.button--outline.button--active,.button--outline:active,.button--outline:hover,:root{--ifm-button-color:var(--ifm-font-color-base-inverse)}.menu__link:hover,a{transition:color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.navbar--dark,:root{--ifm-navbar-link-hover-color:var(--ifm-color-primary)}.menu,.navbar-sidebar{overflow-x:hidden}:root,html[data-theme=dark]{--ifm-color-emphasis-500:var(--ifm-color-gray-500)}.toggleButton_gllP,html{-webkit-tap-highlight-color:transparent}*,.DocSearch-Container,.DocSearch-Container *,.bc-loader{box-sizing:border-box}:root{--ifm-color-scheme:light;--ifm-dark-value:10%;--ifm-darker-value:15%;--ifm-darkest-value:30%;--ifm-light-value:15%;--ifm-lighter-value:30%;--ifm-lightest-value:50%;--ifm-contrast-background-value:90%;--ifm-contrast-foreground-value:70%;--ifm-contrast-background-dark-value:70%;--ifm-contrast-foreground-dark-value:90%;--ifm-color-primary:#3578e5;--ifm-color-secondary:#ebedf0;--ifm-color-success:#00a400;--ifm-color-info:#54c7ec;--ifm-color-warning:#ffba00;--ifm-color-danger:#fa383e;--ifm-color-primary-dark:#306cce;--ifm-color-primary-darker:#2d66c3;--ifm-color-primary-darkest:#2554a0;--ifm-color-primary-light:#538ce9;--ifm-color-primary-lighter:#72a1ed;--ifm-color-primary-lightest:#9abcf2;--ifm-color-primary-contrast-background:#ebf2fc;--ifm-color-primary-contrast-foreground:#102445;--ifm-color-secondary-dark:#d4d5d8;--ifm-color-secondary-darker:#c8c9cc;--ifm-color-secondary-darkest:#a4a6a8;--ifm-color-secondary-light:#eef0f2;--ifm-color-secondary-lighter:#f1f2f5;--ifm-color-secondary-lightest:#f5f6f8;--ifm-color-secondary-contrast-background:#fdfdfe;--ifm-color-secondary-contrast-foreground:#474748;--ifm-color-success-dark:#009400;--ifm-color-success-darker:#008b00;--ifm-color-success-darkest:#007300;--ifm-color-success-light:#26b226;--ifm-color-success-lighter:#4dbf4d;--ifm-color-success-lightest:#80d280;--ifm-color-success-contrast-background:#e6f6e6;--ifm-color-success-contrast-foreground:#003100;--ifm-color-info-dark:#4cb3d4;--ifm-color-info-darker:#47a9c9;--ifm-color-info-darkest:#3b8ba5;--ifm-color-info-light:#6ecfef;--ifm-color-info-lighter:#87d8f2;--ifm-color-info-lightest:#aae3f6;--ifm-color-info-contrast-background:#eef9fd;--ifm-color-info-contrast-foreground:#193c47;--ifm-color-warning-dark:#e6a700;--ifm-color-warning-darker:#d99e00;--ifm-color-warning-darkest:#b38200;--ifm-color-warning-light:#ffc426;--ifm-color-warning-lighter:#ffcf4d;--ifm-color-warning-lightest:#ffdd80;--ifm-color-warning-contrast-background:#fff8e6;--ifm-color-warning-contrast-foreground:#4d3800;--ifm-color-danger-dark:#e13238;--ifm-color-danger-darker:#d53035;--ifm-color-danger-darkest:#af272b;--ifm-color-danger-light:#fb565b;--ifm-color-danger-lighter:#fb7478;--ifm-color-danger-lightest:#fd9c9f;--ifm-color-danger-contrast-background:#ffebec;--ifm-color-danger-contrast-foreground:#4b1113;--ifm-color-white:#fff;--ifm-color-black:#000;--ifm-color-gray-0:var(--ifm-color-white);--ifm-color-gray-100:#f5f6f7;--ifm-color-gray-200:#ebedf0;--ifm-color-gray-300:#dadde1;--ifm-color-gray-400:#ccd0d5;--ifm-color-gray-500:#bec3c9;--ifm-color-gray-600:#8d949e;--ifm-color-gray-700:#606770;--ifm-color-gray-800:#444950;--ifm-color-gray-900:#1c1e21;--ifm-color-gray-1000:var(--ifm-color-black);--ifm-color-emphasis-0:var(--ifm-color-gray-0);--ifm-color-emphasis-100:var(--ifm-color-gray-100);--ifm-color-emphasis-200:var(--ifm-color-gray-200);--ifm-color-emphasis-300:var(--ifm-color-gray-300);--ifm-color-emphasis-400:var(--ifm-color-gray-400);--ifm-color-emphasis-600:var(--ifm-color-gray-600);--ifm-color-emphasis-700:var(--ifm-color-gray-700);--ifm-color-emphasis-800:var(--ifm-color-gray-800);--ifm-color-emphasis-900:var(--ifm-color-gray-900);--ifm-color-emphasis-1000:var(--ifm-color-gray-1000);--ifm-color-content:var(--ifm-color-emphasis-900);--ifm-color-content-inverse:var(--ifm-color-emphasis-0);--ifm-color-content-secondary:#525860;--ifm-background-color:#0000;--ifm-background-surface-color:var(--ifm-color-content-inverse);--ifm-global-border-width:1px;--ifm-global-radius:0.4rem;--ifm-hover-overlay:#0000000d;--ifm-font-color-base:var(--ifm-color-content);--ifm-font-color-base-inverse:var(--ifm-color-content-inverse);--ifm-font-color-secondary:var(--ifm-color-content-secondary);--ifm-font-family-base:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--ifm-font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ifm-font-size-base:100%;--ifm-font-weight-light:300;--ifm-font-weight-normal:400;--ifm-font-weight-semibold:500;--ifm-font-weight-bold:700;--ifm-font-weight-base:var(--ifm-font-weight-normal);--ifm-line-height-base:1.65;--ifm-global-spacing:1rem;--ifm-spacing-vertical:var(--ifm-global-spacing);--ifm-spacing-horizontal:var(--ifm-global-spacing);--ifm-transition-fast:200ms;--ifm-transition-slow:400ms;--ifm-transition-timing-default:cubic-bezier(0.08,0.52,0.52,1);--ifm-global-shadow-lw:0 1px 2px 0 #0000001a;--ifm-global-shadow-md:0 5px 40px #0003;--ifm-global-shadow-tl:0 12px 28px 0 #0003,0 2px 4px 0 #0000001a;--ifm-z-index-dropdown:100;--ifm-z-index-fixed:200;--ifm-z-index-overlay:400;--ifm-container-width:1140px;--ifm-container-width-xl:1320px;--ifm-code-background:#f6f7f8;--ifm-code-border-radius:var(--ifm-global-radius);--ifm-code-font-size:90%;--ifm-code-padding-horizontal:0.1rem;--ifm-code-padding-vertical:0.1rem;--ifm-pre-background:var(--ifm-code-background);--ifm-pre-border-radius:var(--ifm-code-border-radius);--ifm-pre-color:inherit;--ifm-pre-line-height:1.45;--ifm-pre-padding:1rem;--ifm-heading-color:inherit;--ifm-heading-margin-top:0;--ifm-heading-margin-bottom:var(--ifm-spacing-vertical);--ifm-heading-font-family:var(--ifm-font-family-base);--ifm-heading-font-weight:var(--ifm-font-weight-bold);--ifm-heading-line-height:1.25;--ifm-h1-font-size:2rem;--ifm-h2-font-size:1.5rem;--ifm-h3-font-size:1.25rem;--ifm-h4-font-size:1rem;--ifm-h5-font-size:0.875rem;--ifm-h6-font-size:0.85rem;--ifm-image-alignment-padding:1.25rem;--ifm-leading-desktop:1.25;--ifm-leading:calc(var(--ifm-leading-desktop)*1rem);--ifm-list-left-padding:2rem;--ifm-list-margin:1rem;--ifm-list-item-margin:0.25rem;--ifm-list-paragraph-margin:1rem;--ifm-table-cell-padding:0.75rem;--ifm-table-background:#0000;--ifm-table-stripe-background:#00000008;--ifm-table-border-width:1px;--ifm-table-border-color:var(--ifm-color-emphasis-300);--ifm-table-head-background:inherit;--ifm-table-head-color:inherit;--ifm-table-head-font-weight:var(--ifm-font-weight-bold);--ifm-table-cell-color:inherit;--ifm-link-color:var(--ifm-color-primary);--ifm-link-decoration:none;--ifm-link-hover-color:var(--ifm-link-color);--ifm-link-hover-decoration:underline;--ifm-paragraph-margin-bottom:var(--ifm-leading);--ifm-blockquote-font-size:var(--ifm-font-size-base);--ifm-blockquote-border-left-width:2px;--ifm-blockquote-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-blockquote-padding-vertical:0;--ifm-blockquote-shadow:none;--ifm-blockquote-color:var(--ifm-color-emphasis-800);--ifm-blockquote-border-color:var(--ifm-color-emphasis-300);--ifm-hr-background-color:var(--ifm-color-emphasis-500);--ifm-hr-height:1px;--ifm-hr-margin-vertical:1.5rem;--ifm-scrollbar-size:7px;--ifm-scrollbar-track-background-color:#f1f1f1;--ifm-scrollbar-thumb-background-color:silver;--ifm-scrollbar-thumb-hover-background-color:#a7a7a7;--ifm-alert-background-color:inherit;--ifm-alert-border-color:inherit;--ifm-alert-border-radius:var(--ifm-global-radius);--ifm-alert-border-width:0px;--ifm-alert-border-left-width:5px;--ifm-alert-color:var(--ifm-font-color-base);--ifm-alert-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-alert-padding-vertical:var(--ifm-spacing-vertical);--ifm-alert-shadow:var(--ifm-global-shadow-lw);--ifm-avatar-intro-margin:1rem;--ifm-avatar-intro-alignment:inherit;--ifm-avatar-photo-size:3rem;--ifm-badge-background-color:inherit;--ifm-badge-border-color:inherit;--ifm-badge-border-radius:var(--ifm-global-radius);--ifm-badge-border-width:var(--ifm-global-border-width);--ifm-badge-color:var(--ifm-color-white);--ifm-badge-padding-horizontal:calc(var(--ifm-spacing-horizontal)*0.5);--ifm-badge-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-breadcrumb-border-radius:1.5rem;--ifm-breadcrumb-spacing:0.5rem;--ifm-breadcrumb-color-active:var(--ifm-color-primary);--ifm-breadcrumb-item-background-active:var(--ifm-hover-overlay);--ifm-breadcrumb-padding-horizontal:0.8rem;--ifm-breadcrumb-padding-vertical:0.4rem;--ifm-breadcrumb-size-multiplier:1;--ifm-breadcrumb-separator:url('data:image/svg+xml;utf8,');--ifm-breadcrumb-separator-filter:none;--ifm-breadcrumb-separator-size:0.5rem;--ifm-breadcrumb-separator-size-multiplier:1.25;--ifm-button-background-color:inherit;--ifm-button-border-color:var(--ifm-button-background-color);--ifm-button-border-width:var(--ifm-global-border-width);--ifm-button-font-weight:var(--ifm-font-weight-bold);--ifm-button-padding-horizontal:1.5rem;--ifm-button-padding-vertical:0.375rem;--ifm-button-size-multiplier:1;--ifm-button-transition-duration:var(--ifm-transition-fast);--ifm-button-border-radius:calc(var(--ifm-global-radius)*var(--ifm-button-size-multiplier));--ifm-button-group-spacing:2px;--ifm-card-background-color:var(--ifm-background-surface-color);--ifm-card-border-radius:calc(var(--ifm-global-radius)*2);--ifm-card-horizontal-spacing:var(--ifm-global-spacing);--ifm-card-vertical-spacing:var(--ifm-global-spacing);--ifm-toc-border-color:var(--ifm-color-emphasis-300);--ifm-toc-link-color:var(--ifm-color-content-secondary);--ifm-toc-padding-vertical:0.5rem;--ifm-toc-padding-horizontal:0.5rem;--ifm-dropdown-background-color:var(--ifm-background-surface-color);--ifm-dropdown-font-weight:var(--ifm-font-weight-semibold);--ifm-dropdown-link-color:var(--ifm-font-color-base);--ifm-dropdown-hover-background-color:var(--ifm-hover-overlay);--ifm-footer-background-color:var(--ifm-color-emphasis-100);--ifm-footer-color:inherit;--ifm-footer-link-color:var(--ifm-color-emphasis-700);--ifm-footer-link-hover-color:var(--ifm-color-primary);--ifm-footer-link-horizontal-spacing:0.5rem;--ifm-footer-padding-horizontal:calc(var(--ifm-spacing-horizontal)*2);--ifm-footer-padding-vertical:calc(var(--ifm-spacing-vertical)*2);--ifm-footer-title-color:inherit;--ifm-footer-logo-max-width:min(30rem,90vw);--ifm-hero-background-color:var(--ifm-background-surface-color);--ifm-hero-text-color:var(--ifm-color-emphasis-800);--ifm-menu-color:var(--ifm-color-emphasis-700);--ifm-menu-color-active:var(--ifm-color-primary);--ifm-menu-color-background-active:var(--ifm-hover-overlay);--ifm-menu-color-background-hover:var(--ifm-hover-overlay);--ifm-menu-link-padding-horizontal:0.75rem;--ifm-menu-link-padding-vertical:0.375rem;--ifm-menu-link-sublist-icon:url('data:image/svg+xml;utf8,');--ifm-menu-link-sublist-icon-filter:none;--ifm-navbar-background-color:var(--ifm-background-surface-color);--ifm-navbar-height:3.75rem;--ifm-navbar-item-padding-horizontal:0.75rem;--ifm-navbar-item-padding-vertical:0.25rem;--ifm-navbar-link-color:var(--ifm-font-color-base);--ifm-navbar-link-active-color:var(--ifm-link-color);--ifm-navbar-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-navbar-padding-vertical:calc(var(--ifm-spacing-vertical)*0.5);--ifm-navbar-shadow:var(--ifm-global-shadow-lw);--ifm-navbar-search-input-background-color:var(--ifm-color-emphasis-200);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-800);--ifm-navbar-search-input-placeholder-color:var(--ifm-color-emphasis-500);--ifm-navbar-search-input-icon:url('data:image/svg+xml;utf8,');--ifm-navbar-sidebar-width:83vw;--ifm-pagination-border-radius:var(--ifm-global-radius);--ifm-pagination-color-active:var(--ifm-color-primary);--ifm-pagination-font-size:1rem;--ifm-pagination-item-active-background:var(--ifm-hover-overlay);--ifm-pagination-page-spacing:0.2em;--ifm-pagination-padding-horizontal:calc(var(--ifm-spacing-horizontal)*1);--ifm-pagination-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-pagination-nav-border-radius:var(--ifm-global-radius);--ifm-pagination-nav-color-hover:var(--ifm-color-primary);--ifm-pills-color-active:var(--ifm-color-primary);--ifm-pills-color-background-active:var(--ifm-hover-overlay);--ifm-pills-spacing:0.125rem;--ifm-tabs-color:var(--ifm-font-color-secondary);--ifm-tabs-color-active:var(--ifm-color-primary);--ifm-tabs-color-active-border:var(--ifm-tabs-color-active);--ifm-tabs-padding-horizontal:1rem;--ifm-tabs-padding-vertical:1rem;--docusaurus-progress-bar-color:var(--ifm-color-primary);--ifm-font-family-base:"IamwriterquattroS";--docusaurus-announcement-bar-height:auto;--docusaurus-collapse-button-bg:#0000;--docusaurus-collapse-button-bg-hover:#0000001a;--doc-sidebar-width:300px;--doc-sidebar-hidden-width:30px;--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:#656c85cc;--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 #ffffff80,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 #1e235a66;--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 #45629b1f;--docsearch-primary-color:var(--ifm-color-primary);--docsearch-text-color:var(--ifm-font-color-base);--docusaurus-tag-list-border:var(--ifm-color-emphasis-300)}.badge--danger,.badge--info,.badge--primary,.badge--secondary,.badge--success,.badge--warning{--ifm-badge-border-color:var(--ifm-badge-background-color)}.button--link,.button--outline{--ifm-button-background-color:#0000}html{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--ifm-background-color);color:var(--ifm-font-color-base);color-scheme:var(--ifm-color-scheme);font:var(--ifm-font-size-base)/var(--ifm-line-height-base) var(--ifm-font-family-base);text-rendering:optimizelegibility}iframe{border:0;color-scheme:auto}.container{margin:0 auto;max-width:var(--ifm-container-width)}.container--fluid{max-width:inherit}.row{display:flex;flex-wrap:wrap;margin:0 calc(var(--ifm-spacing-horizontal)*-1)}.margin-bottom--none,.margin-vert--none,.markdown>:last-child{margin-bottom:0!important}.margin-top--none,.margin-vert--none{margin-top:0!important}.row--no-gutters{margin-left:0;margin-right:0}.margin-horiz--none,.margin-right--none{margin-right:0!important}.row--no-gutters>.col{padding-left:0;padding-right:0}.row--align-top{align-items:flex-start}.row--align-bottom{align-items:flex-end}.row--align-center{align-items:center}.row--align-stretch{align-items:stretch}.row--align-baseline{align-items:baseline}.col{--ifm-col-width:100%;flex:1 0;margin-left:0;max-width:var(--ifm-col-width)}.padding-bottom--none,.padding-vert--none{padding-bottom:0!important}.padding-top--none,.padding-vert--none{padding-top:0!important}.padding-horiz--none,.padding-left--none{padding-left:0!important}.padding-horiz--none,.padding-right--none{padding-right:0!important}.col[class*=col--]{flex:0 0 var(--ifm-col-width)}.col--1{--ifm-col-width:8.33333%}.col--offset-1{margin-left:8.33333%}.col--2{--ifm-col-width:16.66667%}.col--offset-2{margin-left:16.66667%}.col--3{--ifm-col-width:25%}.col--offset-3{margin-left:25%}.col--4{--ifm-col-width:33.33333%}.col--offset-4{margin-left:33.33333%}.col--5{--ifm-col-width:41.66667%}.col--offset-5{margin-left:41.66667%}.col--6{--ifm-col-width:50%}.col--offset-6{margin-left:50%}.col--7{--ifm-col-width:58.33333%}.col--offset-7{margin-left:58.33333%}.col--8{--ifm-col-width:66.66667%}.col--offset-8{margin-left:66.66667%}.col--9{--ifm-col-width:75%}.col--offset-9{margin-left:75%}.col--10{--ifm-col-width:83.33333%}.col--offset-10{margin-left:83.33333%}.col--11{--ifm-col-width:91.66667%}.col--offset-11{margin-left:91.66667%}.col--12{--ifm-col-width:100%}.col--offset-12{margin-left:100%}.margin-horiz--none,.margin-left--none{margin-left:0!important}.margin--none{margin:0!important}.margin-bottom--xs,.margin-vert--xs{margin-bottom:.25rem!important}.margin-top--xs,.margin-vert--xs{margin-top:.25rem!important}.margin-horiz--xs,.margin-left--xs{margin-left:.25rem!important}.margin-horiz--xs,.margin-right--xs{margin-right:.25rem!important}.margin--xs{margin:.25rem!important}.margin-bottom--sm,.margin-vert--sm{margin-bottom:.5rem!important}.margin-top--sm,.margin-vert--sm{margin-top:.5rem!important}.margin-horiz--sm,.margin-left--sm{margin-left:.5rem!important}.margin-horiz--sm,.margin-right--sm{margin-right:.5rem!important}.margin--sm{margin:.5rem!important}.margin-bottom--md,.margin-vert--md{margin-bottom:1rem!important}.margin-top--md,.margin-vert--md{margin-top:1rem!important}.margin-horiz--md,.margin-left--md{margin-left:1rem!important}.margin-horiz--md,.margin-right--md{margin-right:1rem!important}.margin--md{margin:1rem!important}.margin-bottom--lg,.margin-vert--lg{margin-bottom:2rem!important}.margin-top--lg,.margin-vert--lg{margin-top:2rem!important}.margin-horiz--lg,.margin-left--lg{margin-left:2rem!important}.margin-horiz--lg,.margin-right--lg{margin-right:2rem!important}.margin--lg{margin:2rem!important}.margin-bottom--xl,.margin-vert--xl{margin-bottom:5rem!important}.margin-top--xl,.margin-vert--xl{margin-top:5rem!important}.margin-horiz--xl,.margin-left--xl{margin-left:5rem!important}.margin-horiz--xl,.margin-right--xl{margin-right:5rem!important}.margin--xl{margin:5rem!important}.padding--none{padding:0!important}.padding-bottom--xs,.padding-vert--xs{padding-bottom:.25rem!important}.padding-top--xs,.padding-vert--xs{padding-top:.25rem!important}.padding-horiz--xs,.padding-left--xs{padding-left:.25rem!important}.padding-horiz--xs,.padding-right--xs{padding-right:.25rem!important}.padding--xs{padding:.25rem!important}.padding-bottom--sm,.padding-vert--sm{padding-bottom:.5rem!important}.padding-top--sm,.padding-vert--sm{padding-top:.5rem!important}.padding-horiz--sm,.padding-left--sm{padding-left:.5rem!important}.padding-horiz--sm,.padding-right--sm{padding-right:.5rem!important}.padding--sm{padding:.5rem!important}.padding-bottom--md,.padding-vert--md{padding-bottom:1rem!important}.padding-top--md,.padding-vert--md{padding-top:1rem!important}.padding-horiz--md,.padding-left--md{padding-left:1rem!important}.padding-horiz--md,.padding-right--md{padding-right:1rem!important}.padding--md{padding:1rem!important}.padding-bottom--lg,.padding-vert--lg{padding-bottom:2rem!important}.padding-top--lg,.padding-vert--lg{padding-top:2rem!important}.padding-horiz--lg,.padding-left--lg{padding-left:2rem!important}.padding-horiz--lg,.padding-right--lg{padding-right:2rem!important}.padding--lg{padding:2rem!important}.padding-bottom--xl,.padding-vert--xl{padding-bottom:5rem!important}.padding-top--xl,.padding-vert--xl{padding-top:5rem!important}.padding-horiz--xl,.padding-left--xl{padding-left:5rem!important}.padding-horiz--xl,.padding-right--xl{padding-right:5rem!important}.padding--xl{padding:5rem!important}code{background-color:var(--ifm-code-background);border:.1rem solid #0000001a;border-radius:var(--ifm-code-border-radius);font-family:var(--ifm-font-family-monospace);font-size:var(--ifm-code-font-size);padding:var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal)}a code{color:inherit}pre{background-color:var(--ifm-pre-background);border-radius:var(--ifm-pre-border-radius);color:var(--ifm-pre-color);font:var(--ifm-code-font-size)/var(--ifm-pre-line-height) var(--ifm-font-family-monospace);padding:var(--ifm-pre-padding)}pre code{background-color:initial;border:none;font-size:100%;line-height:inherit;padding:0}kbd{background-color:var(--ifm-color-emphasis-0);border:1px solid var(--ifm-color-emphasis-400);border-radius:.2rem;box-shadow:inset 0 -1px 0 var(--ifm-color-emphasis-400);color:var(--ifm-color-emphasis-800);font:80% var(--ifm-font-family-monospace);padding:.15rem .3rem}h1,h2,h3,h4,h5,h6{color:var(--ifm-heading-color);font-family:var(--ifm-heading-font-family);font-weight:var(--ifm-heading-font-weight);line-height:var(--ifm-heading-line-height);margin:var(--ifm-heading-margin-top) 0 var(--ifm-heading-margin-bottom) 0}h1{font-size:var(--ifm-h1-font-size)}h2{font-size:var(--ifm-h2-font-size)}h3{font-size:var(--ifm-h3-font-size)}h4{font-size:var(--ifm-h4-font-size)}h5{font-size:var(--ifm-h5-font-size)}h6{font-size:var(--ifm-h6-font-size)}.max-width-100,img{max-width:100%}img[align=right]{padding-left:var(--image-alignment-padding)}img[align=left]{padding-right:var(--image-alignment-padding)}.markdown{--ifm-h1-vertical-rhythm-top:3;--ifm-h2-vertical-rhythm-top:2;--ifm-h3-vertical-rhythm-top:1.5;--ifm-heading-vertical-rhythm-top:1.25;--ifm-h1-vertical-rhythm-bottom:1.25;--ifm-heading-vertical-rhythm-bottom:1}.markdown:after,.markdown:before{content:"";display:table}.markdown:after{clear:both}.markdown h1:first-child{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-h1-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown>h2{--ifm-h2-font-size:2rem;margin-top:calc(var(--ifm-h2-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h3{--ifm-h3-font-size:1.5rem;margin-top:calc(var(--ifm-h3-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h4,.markdown>h5,.markdown>h6{margin-top:calc(var(--ifm-heading-vertical-rhythm-top)*var(--ifm-leading))}.markdown>p,.markdown>pre,.markdown>ul{margin-bottom:var(--ifm-leading)}.markdown li>p{margin-top:var(--ifm-list-paragraph-margin)}.markdown li+li{margin-top:var(--ifm-list-item-margin)}ol,ul{margin:0 0 var(--ifm-list-margin);padding-left:var(--ifm-list-left-padding)}ol ol,ul ol{list-style-type:lower-roman}ol ol ol,ol ul ol,ul ol ol,ul ul ol{list-style-type:lower-alpha}table{border-collapse:collapse;display:block;margin-bottom:var(--ifm-spacing-vertical)}table thead tr{border-bottom:2px solid var(--ifm-table-border-color)}table thead,table tr:nth-child(2n){background-color:var(--ifm-table-stripe-background)}table tr{background-color:var(--ifm-table-background);border-top:var(--ifm-table-border-width) solid var(--ifm-table-border-color)}table td,table th{border:var(--ifm-table-border-width) solid var(--ifm-table-border-color);padding:var(--ifm-table-cell-padding)}table th{background-color:var(--ifm-table-head-background);color:var(--ifm-table-head-color);font-weight:var(--ifm-table-head-font-weight)}table td{color:var(--ifm-table-cell-color)}strong{font-weight:var(--ifm-font-weight-bold)}a{color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}a:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button:hover,.text--no-decoration,.text--no-decoration:hover,a:not([href]){text-decoration:none}p{margin:0 0 var(--ifm-paragraph-margin-bottom)}blockquote{border-left:var(--ifm-blockquote-border-left-width) solid var(--ifm-blockquote-border-color);box-shadow:var(--ifm-blockquote-shadow);color:var(--ifm-blockquote-color);font-size:var(--ifm-blockquote-font-size);padding:var(--ifm-blockquote-padding-vertical) var(--ifm-blockquote-padding-horizontal)}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}hr{background-color:var(--ifm-hr-background-color);border:0;height:var(--ifm-hr-height);margin:var(--ifm-hr-margin-vertical) 0}.shadow--lw{box-shadow:var(--ifm-global-shadow-lw)!important}.shadow--md{box-shadow:var(--ifm-global-shadow-md)!important}.shadow--tl{box-shadow:var(--ifm-global-shadow-tl)!important}.text--primary{color:var(--ifm-color-primary)}.text--secondary{color:var(--ifm-color-secondary)}.text--success{color:var(--ifm-color-success)}.text--info{color:var(--ifm-color-info)}.text--warning{color:var(--ifm-color-warning)}.text--danger{color:var(--ifm-color-danger)}.text--center{text-align:center}.text--left{text-align:left}.text--justify{text-align:justify}.text--right{text-align:right}.text--capitalize{text-transform:capitalize}.text--lowercase{text-transform:lowercase}.admonitionHeading_Gvgb,.alert__heading,.footer-ls-section-title,.hp-section-header,.text--uppercase{text-transform:uppercase}.text--light{font-weight:var(--ifm-font-weight-light)}.text--normal{font-weight:var(--ifm-font-weight-normal)}.text--semibold{font-weight:var(--ifm-font-weight-semibold)}.text--bold{font-weight:var(--ifm-font-weight-bold)}.text--italic{font-style:italic}.text--truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text--break{word-wrap:break-word!important;word-break:break-word!important}.clean-btn{background:none;border:none;color:inherit;cursor:pointer;font-family:inherit;padding:0}.alert,.alert .close{color:var(--ifm-alert-foreground-color)}.clean-list{list-style:none;padding-left:0}.alert--primary{--ifm-alert-background-color:var(--ifm-color-primary-contrast-background);--ifm-alert-background-color-highlight:#3578e526;--ifm-alert-foreground-color:var(--ifm-color-primary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-primary-dark)}.alert--secondary{--ifm-alert-background-color:var(--ifm-color-secondary-contrast-background);--ifm-alert-background-color-highlight:#ebedf026;--ifm-alert-foreground-color:var(--ifm-color-secondary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-secondary-dark)}.alert--success{--ifm-alert-background-color:var(--ifm-color-success-contrast-background);--ifm-alert-background-color-highlight:#00a40026;--ifm-alert-foreground-color:var(--ifm-color-success-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-success-dark)}.alert--info{--ifm-alert-background-color:var(--ifm-color-info-contrast-background);--ifm-alert-background-color-highlight:#54c7ec26;--ifm-alert-foreground-color:var(--ifm-color-info-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-info-dark)}.alert--warning{--ifm-alert-background-color:var(--ifm-color-warning-contrast-background);--ifm-alert-background-color-highlight:#ffba0026;--ifm-alert-foreground-color:var(--ifm-color-warning-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-warning-dark)}.alert--danger{--ifm-alert-background-color:var(--ifm-color-danger-contrast-background);--ifm-alert-background-color-highlight:#fa383e26;--ifm-alert-foreground-color:var(--ifm-color-danger-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-danger-dark)}.alert{--ifm-code-background:var(--ifm-alert-background-color-highlight);--ifm-link-color:var(--ifm-alert-foreground-color);--ifm-link-hover-color:var(--ifm-alert-foreground-color);--ifm-link-decoration:underline;--ifm-tabs-color:var(--ifm-alert-foreground-color);--ifm-tabs-color-active:var(--ifm-alert-foreground-color);--ifm-tabs-color-active-border:var(--ifm-alert-border-color);background-color:var(--ifm-alert-background-color);border:var(--ifm-alert-border-width) solid var(--ifm-alert-border-color);border-left-width:var(--ifm-alert-border-left-width);border-radius:var(--ifm-alert-border-radius);box-shadow:var(--ifm-alert-shadow);padding:var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal)}.alert__heading{align-items:center;display:flex;font:700 var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family);margin-bottom:.5rem}.alert__icon{display:inline-flex;margin-right:.4em}.alert__icon svg{fill:var(--ifm-alert-foreground-color);stroke:var(--ifm-alert-foreground-color);stroke-width:0}.alert .close{margin:calc(var(--ifm-alert-padding-vertical)*-1) calc(var(--ifm-alert-padding-horizontal)*-1) 0 0;opacity:.75}.alert .close:focus,.alert .close:hover{opacity:1}.alert a{text-decoration-color:var(--ifm-alert-border-color)}.alert a:hover{text-decoration-thickness:2px}.avatar{column-gap:var(--ifm-avatar-intro-margin);display:flex}.avatar__photo{border-radius:50%;display:block;height:var(--ifm-avatar-photo-size);overflow:hidden;width:var(--ifm-avatar-photo-size)}.card--full-height,.navbar__logo img,body,html{height:100%}.avatar__photo--sm{--ifm-avatar-photo-size:2rem}.avatar__photo--lg{--ifm-avatar-photo-size:4rem}.avatar__photo--xl{--ifm-avatar-photo-size:6rem}.avatar__intro{display:flex;flex:1 1;flex-direction:column;justify-content:center;text-align:var(--ifm-avatar-intro-alignment)}.badge,.breadcrumbs__item,.breadcrumbs__link,.button,.dropdown>.navbar__link:after{display:inline-block}.avatar__name{font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base)}.avatar__subtitle{margin-top:.25rem}.avatar--vertical{--ifm-avatar-intro-alignment:center;--ifm-avatar-intro-margin:0.5rem;align-items:center;flex-direction:column}.badge{background-color:var(--ifm-badge-background-color);border:var(--ifm-badge-border-width) solid var(--ifm-badge-border-color);border-radius:var(--ifm-badge-border-radius);color:var(--ifm-badge-color);font-size:75%;font-weight:var(--ifm-font-weight-bold);line-height:1;padding:var(--ifm-badge-padding-vertical) var(--ifm-badge-padding-horizontal)}.badge--primary{--ifm-badge-background-color:var(--ifm-color-primary)}.badge--secondary{--ifm-badge-background-color:var(--ifm-color-secondary);color:var(--ifm-color-black)}.breadcrumbs__link,.button.button--secondary.button--outline:not(.button--active):not(:hover){color:var(--ifm-font-color-base)}.badge--success{--ifm-badge-background-color:var(--ifm-color-success)}.badge--info{--ifm-badge-background-color:var(--ifm-color-info)}.badge--warning{--ifm-badge-background-color:var(--ifm-color-warning)}.badge--danger{--ifm-badge-background-color:var(--ifm-color-danger)}.breadcrumbs{margin-bottom:0;padding-left:0}.breadcrumbs__item:not(:last-child):after{background:var(--ifm-breadcrumb-separator) center;content:" ";display:inline-block;filter:var(--ifm-breadcrumb-separator-filter);height:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier));margin:0 var(--ifm-breadcrumb-spacing);opacity:.5;width:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier))}.breadcrumbs__item--active .breadcrumbs__link{background:var(--ifm-breadcrumb-item-background-active);color:var(--ifm-breadcrumb-color-active)}.breadcrumbs__link{border-radius:var(--ifm-breadcrumb-border-radius);font-size:calc(1rem*var(--ifm-breadcrumb-size-multiplier));padding:calc(var(--ifm-breadcrumb-padding-vertical)*var(--ifm-breadcrumb-size-multiplier)) calc(var(--ifm-breadcrumb-padding-horizontal)*var(--ifm-breadcrumb-size-multiplier));transition-duration:var(--ifm-transition-fast);transition-property:background,color}.breadcrumbs__link:any-link:hover,.breadcrumbs__link:link:hover,.breadcrumbs__link:visited:hover,area[href].breadcrumbs__link:hover{background:var(--ifm-breadcrumb-item-background-active);text-decoration:none}.breadcrumbs--sm{--ifm-breadcrumb-size-multiplier:0.8}.breadcrumbs--lg{--ifm-breadcrumb-size-multiplier:1.2}.button{background-color:var(--ifm-button-background-color);border:var(--ifm-button-border-width) solid var(--ifm-button-border-color);border-radius:var(--ifm-button-border-radius);cursor:pointer;font-size:calc(.875rem*var(--ifm-button-size-multiplier));font-weight:var(--ifm-button-font-weight);line-height:1.5;padding:calc(var(--ifm-button-padding-vertical)*var(--ifm-button-size-multiplier)) calc(var(--ifm-button-padding-horizontal)*var(--ifm-button-size-multiplier));text-align:center;transition-duration:var(--ifm-button-transition-duration);transition-property:color,background,border-color;-webkit-user-select:none;user-select:none;white-space:nowrap}.button,.button:hover{color:var(--ifm-button-color)}.button--outline{--ifm-button-color:var(--ifm-button-border-color)}.button--outline:hover{--ifm-button-background-color:var(--ifm-button-border-color)}.button--link{--ifm-button-border-color:#0000;color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}.button--link.button--active,.button--link:active,.button--link:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button.disabled,.button:disabled,.button[disabled]{opacity:.65;pointer-events:none}.button--sm{--ifm-button-size-multiplier:0.8}.button--lg{--ifm-button-size-multiplier:1.35}.button--block{display:block;width:100%}.button.button--secondary{color:var(--ifm-color-gray-900)}:where(.button--primary){--ifm-button-background-color:var(--ifm-color-primary);--ifm-button-border-color:var(--ifm-color-primary)}:where(.button--primary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-primary-dark);--ifm-button-border-color:var(--ifm-color-primary-dark)}.button--primary.button--active,.button--primary:active{--ifm-button-background-color:var(--ifm-color-primary-darker);--ifm-button-border-color:var(--ifm-color-primary-darker)}:where(.button--secondary){--ifm-button-background-color:var(--ifm-color-secondary);--ifm-button-border-color:var(--ifm-color-secondary)}:where(.button--secondary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-secondary-dark);--ifm-button-border-color:var(--ifm-color-secondary-dark)}.button--secondary.button--active,.button--secondary:active{--ifm-button-background-color:var(--ifm-color-secondary-darker);--ifm-button-border-color:var(--ifm-color-secondary-darker)}:where(.button--success){--ifm-button-background-color:var(--ifm-color-success);--ifm-button-border-color:var(--ifm-color-success)}:where(.button--success):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-success-dark);--ifm-button-border-color:var(--ifm-color-success-dark)}.button--success.button--active,.button--success:active{--ifm-button-background-color:var(--ifm-color-success-darker);--ifm-button-border-color:var(--ifm-color-success-darker)}:where(.button--info){--ifm-button-background-color:var(--ifm-color-info);--ifm-button-border-color:var(--ifm-color-info)}:where(.button--info):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-info-dark);--ifm-button-border-color:var(--ifm-color-info-dark)}.button--info.button--active,.button--info:active{--ifm-button-background-color:var(--ifm-color-info-darker);--ifm-button-border-color:var(--ifm-color-info-darker)}:where(.button--warning){--ifm-button-background-color:var(--ifm-color-warning);--ifm-button-border-color:var(--ifm-color-warning)}:where(.button--warning):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-warning-dark);--ifm-button-border-color:var(--ifm-color-warning-dark)}.button--warning.button--active,.button--warning:active{--ifm-button-background-color:var(--ifm-color-warning-darker);--ifm-button-border-color:var(--ifm-color-warning-darker)}:where(.button--danger){--ifm-button-background-color:var(--ifm-color-danger);--ifm-button-border-color:var(--ifm-color-danger)}:where(.button--danger):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-danger-dark);--ifm-button-border-color:var(--ifm-color-danger-dark)}.button--danger.button--active,.button--danger:active{--ifm-button-background-color:var(--ifm-color-danger-darker);--ifm-button-border-color:var(--ifm-color-danger-darker)}.button-group{display:inline-flex;gap:var(--ifm-button-group-spacing)}.button-group>.button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.button-group>.button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.button-group--block{display:flex;justify-content:stretch}.button-group--block>.button{flex-grow:1}.card{background-color:var(--ifm-card-background-color);border-radius:var(--ifm-card-border-radius);box-shadow:var(--ifm-global-shadow-lw);display:flex;flex-direction:column;overflow:hidden}.card__image{padding-top:var(--ifm-card-vertical-spacing)}.card__image:first-child{padding-top:0}.card__body,.card__footer,.card__header{padding:var(--ifm-card-vertical-spacing) var(--ifm-card-horizontal-spacing)}.card__body:not(:last-child),.card__footer:not(:last-child),.card__header:not(:last-child){padding-bottom:0}.card__body>:last-child,.card__footer>:last-child,.card__header>:last-child{margin-bottom:0}.card__footer{margin-top:auto}.table-of-contents{font-size:.8rem;margin-bottom:0;padding:var(--ifm-toc-padding-vertical) 0}.table-of-contents,.table-of-contents ul{list-style:none;padding-left:var(--ifm-toc-padding-horizontal)}.table-of-contents li{margin:var(--ifm-toc-padding-vertical) var(--ifm-toc-padding-horizontal)}.table-of-contents__left-border{border-left:1px solid var(--ifm-toc-border-color)}.table-of-contents__link{color:var(--ifm-toc-link-color);display:block}.table-of-contents__link--active,.table-of-contents__link--active code,.table-of-contents__link:hover,.table-of-contents__link:hover code{color:var(--ifm-color-primary);text-decoration:none}.close{color:var(--ifm-color-black);float:right;font-size:1.5rem;font-weight:var(--ifm-font-weight-bold);line-height:1;opacity:.5;padding:1rem;transition:opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.close:hover{opacity:.7}.close:focus{opacity:.8}.dropdown{display:inline-flex;font-weight:var(--ifm-dropdown-font-weight);position:relative;vertical-align:top}.dropdown--hoverable:hover .dropdown__menu,.dropdown--show .dropdown__menu{opacity:1;pointer-events:all;transform:translateY(-1px);visibility:visible}#nprogress,.dropdown__menu,.navbar__item.dropdown .navbar__link:not([href]){pointer-events:none}.dropdown--right .dropdown__menu{left:inherit;right:0}.dropdown--nocaret .navbar__link:after{content:none!important}.dropdown__menu{background-color:var(--ifm-dropdown-background-color);border-radius:var(--ifm-global-radius);box-shadow:var(--ifm-global-shadow-md);left:0;list-style:none;max-height:80vh;min-width:10rem;opacity:0;overflow-y:auto;padding:.5rem;position:absolute;top:calc(100% - var(--ifm-navbar-item-padding-vertical) + .3rem);transform:translateY(-.625rem);transition-duration:var(--ifm-transition-fast);transition-property:opacity,transform,visibility;transition-timing-function:var(--ifm-transition-timing-default);visibility:hidden;z-index:var(--ifm-z-index-dropdown)}.menu__caret,.menu__link,.menu__list-item-collapsible{border-radius:.25rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.dropdown__link{border-radius:.25rem;color:var(--ifm-dropdown-link-color);display:block;font-size:.875rem;margin-top:.2rem;padding:.25rem .5rem;white-space:nowrap}.dropdown__link--active,.dropdown__link:hover{background-color:var(--ifm-dropdown-hover-background-color);color:var(--ifm-dropdown-link-color);text-decoration:none}.dropdown__link--active,.dropdown__link--active:hover{--ifm-dropdown-link-color:var(--ifm-link-color)}.dropdown>.navbar__link:after{border-color:currentcolor #0000;border-style:solid;border-width:.4em .4em 0;content:"";margin-left:.3em;position:relative;top:2px;transform:translateY(-50%)}.details_lb9f[data-collapsed=false].isBrowser_bmU9>summary:before,.details_lb9f[open]:not(.isBrowser_bmU9)>summary:before,.menu__list-item--collapsed .menu__caret:before,.menu__list-item--collapsed .menu__link--sublist:after{transform:rotate(90deg)}.footer{background-color:var(--ifm-footer-background-color);color:var(--ifm-footer-color);padding:var(--ifm-footer-padding-vertical) var(--ifm-footer-padding-horizontal)}.footer--dark{--ifm-footer-background-color:#303846;--ifm-footer-color:var(--ifm-footer-link-color);--ifm-footer-link-color:var(--ifm-color-secondary);--ifm-footer-title-color:var(--ifm-color-white)}.footer__links{margin-bottom:1rem}.footer__link-item{color:var(--ifm-footer-link-color);line-height:2}.footer__link-item:hover{color:var(--ifm-footer-link-hover-color)}.footer__link-separator{margin:0 var(--ifm-footer-link-horizontal-spacing)}.footer__logo{margin-top:1rem;max-width:var(--ifm-footer-logo-max-width)}.docItemContainer_Djhp article>:first-child,.docItemContainer_Djhp header+*,.footer__item{margin-top:0}.footer__title{color:var(--ifm-footer-title-color);font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base);margin-bottom:var(--ifm-heading-margin-bottom)}.menu,.navbar__link{font-weight:var(--ifm-font-weight-semibold)}.footer__items{margin-bottom:0}[type=checkbox]{padding:0}.hero{align-items:center;background-color:var(--ifm-hero-background-color);color:var(--ifm-hero-text-color);display:flex;padding:4rem 2rem}.hero--primary{--ifm-hero-background-color:var(--ifm-color-primary);--ifm-hero-text-color:var(--ifm-font-color-base-inverse)}.hero--dark{--ifm-hero-background-color:#303846;--ifm-hero-text-color:var(--ifm-color-white)}.hero__title{font-size:3rem}.hero__subtitle{font-size:1.5rem}.menu__list{list-style:none;margin:0;padding-left:0}.menu__caret,.menu__link{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu__list .menu__list{flex:0 0 100%;margin-top:.25rem;padding-left:var(--ifm-menu-link-padding-horizontal)}.menu__list-item:not(:first-child){margin-top:.25rem}.menu__list-item--collapsed .menu__list{height:0;overflow:hidden}.menu__list-item-collapsible{display:flex;flex-wrap:wrap;position:relative}.menu__caret:hover,.menu__link:hover,.menu__list-item-collapsible--active,.menu__list-item-collapsible:hover{background:var(--ifm-menu-color-background-hover)}.menu__list-item-collapsible .menu__link--active,.menu__list-item-collapsible .menu__link:hover{background:none!important}.menu__caret,.menu__link{align-items:center;display:flex}.navbar-sidebar,.navbar-sidebar__backdrop{bottom:0;opacity:0;transition-duration:var(--ifm-transition-fast);transition-timing-function:ease-in-out;left:0;top:0;visibility:hidden}.menu__link{color:var(--ifm-menu-color);flex:1;line-height:1.25}.menu__link:hover{color:var(--ifm-menu-color);text-decoration:none}.menu__caret:before,.menu__link--sublist-caret:after{content:"";height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast) linear;width:1.25rem;filter:var(--ifm-menu-link-sublist-icon-filter)}.menu__link--sublist-caret:after{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem;margin-left:auto;min-width:1.25rem}.menu__link--active,.menu__link--active:hover{color:var(--ifm-menu-color-active)}.navbar__brand,.navbar__link{color:var(--ifm-navbar-link-color)}.menu__link--active:not(.menu__link--sublist){background-color:var(--ifm-menu-color-background-active)}.menu__caret:before{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem}.navbar--dark,html[data-theme=dark]{--ifm-menu-link-sublist-icon-filter:invert(100%) sepia(94%) saturate(17%) hue-rotate(223deg) brightness(104%) contrast(98%)}.navbar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-navbar-shadow);height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar,.navbar>.container,.navbar>.container-fluid{display:flex}.navbar--fixed-top{position:sticky;top:0;z-index:var(--ifm-z-index-fixed)}.navbar__inner{display:flex;flex-wrap:wrap;justify-content:space-between;width:100%}.navbar__brand{align-items:center;display:flex;margin-right:1rem;min-width:0}.navbar__brand:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.navbar__title{flex:1 1 auto}.navbar__toggle{display:none;margin-right:.5rem}.navbar__logo{flex:0 0 auto;height:2rem;margin-right:.5rem}.navbar__items{align-items:center;display:flex;flex:1;min-width:0}.navbar__items--center{flex:0 0 auto}.navbar__items--center .navbar__brand{margin:0}.navbar__items--center+.navbar__items--right{flex:1}.navbar__items--right{flex:0 0 auto;justify-content:flex-end}.navbar__items--right>:last-child{padding-right:0}.navbar__item{display:inline-block;padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.navbar__link--active,.navbar__link:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.navbar--dark,.navbar--primary{--ifm-menu-color:var(--ifm-color-gray-300);--ifm-navbar-link-color:var(--ifm-color-gray-100);--ifm-navbar-search-input-background-color:#ffffff1a;--ifm-navbar-search-input-placeholder-color:#ffffff80;color:var(--ifm-color-white)}.navbar--dark{--ifm-navbar-background-color:#242526;--ifm-menu-color-background-active:#ffffff0d;--ifm-navbar-search-input-color:var(--ifm-color-white)}.navbar--primary{--ifm-navbar-background-color:var(--ifm-color-primary);--ifm-navbar-link-hover-color:var(--ifm-color-white);--ifm-menu-color-active:var(--ifm-color-white);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-500)}.navbar__search-input{-webkit-appearance:none;appearance:none;background:var(--ifm-navbar-search-input-background-color) var(--ifm-navbar-search-input-icon) no-repeat .75rem center/1rem 1rem;border:none;border-radius:2rem;color:var(--ifm-navbar-search-input-color);cursor:text;display:inline-block;font-size:.9rem;height:2rem;padding:0 .5rem 0 2.25rem;width:12.5rem}.navbar__search-input::placeholder{color:var(--ifm-navbar-search-input-placeholder-color)}.navbar-sidebar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-global-shadow-md);position:fixed;transform:translate3d(-100%,0,0);transition-property:opacity,visibility,transform;width:var(--ifm-navbar-sidebar-width)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar__items{transform:translateZ(0)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar--show .navbar-sidebar__backdrop{opacity:1;visibility:visible}.navbar-sidebar__backdrop{background-color:#0009;position:fixed;right:0;transition-property:opacity,visibility}.navbar-sidebar__brand{align-items:center;box-shadow:var(--ifm-navbar-shadow);display:flex;flex:1;height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar-sidebar__items{display:flex;height:calc(100% - var(--ifm-navbar-height));transition:transform var(--ifm-transition-fast) ease-in-out}.navbar-sidebar__items--show-secondary{transform:translate3d(calc((var(--ifm-navbar-sidebar-width))*-1),0,0)}.navbar-sidebar__item{flex-shrink:0;padding:.5rem;width:calc(var(--ifm-navbar-sidebar-width))}.navbar-sidebar__back{background:var(--ifm-menu-color-background-active);font-size:15px;font-weight:var(--ifm-button-font-weight);margin:0 0 .2rem -.5rem;padding:.6rem 1.5rem;position:relative;text-align:left;top:-.5rem;width:calc(100% + 1rem)}.navbar-sidebar__close{display:flex;margin-left:auto}.pagination{column-gap:var(--ifm-pagination-page-spacing);display:flex;font-size:var(--ifm-pagination-font-size);padding-left:0}.pagination--sm{--ifm-pagination-font-size:0.8rem;--ifm-pagination-padding-horizontal:0.8rem;--ifm-pagination-padding-vertical:0.2rem}.pagination--lg{--ifm-pagination-font-size:1.2rem;--ifm-pagination-padding-horizontal:1.2rem;--ifm-pagination-padding-vertical:0.3rem}.pagination__item{display:inline-flex}.pagination__item>span{padding:var(--ifm-pagination-padding-vertical)}.pagination__item--active .pagination__link{color:var(--ifm-pagination-color-active)}.pagination__item--active .pagination__link,.pagination__item:not(.pagination__item--active):hover .pagination__link{background:var(--ifm-pagination-item-active-background)}.pagination__item--disabled,.pagination__item[disabled]{opacity:.25;pointer-events:none}.pagination__link{border-radius:var(--ifm-pagination-border-radius);color:var(--ifm-font-color-base);display:inline-block;padding:var(--ifm-pagination-padding-vertical) var(--ifm-pagination-padding-horizontal);transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination__link:hover{text-decoration:none}.pagination-nav{grid-gap:var(--ifm-spacing-horizontal);display:grid;gap:var(--ifm-spacing-horizontal);grid-template-columns:repeat(2,1fr)}.pagination-nav__link{border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-pagination-nav-border-radius);display:block;height:100%;line-height:var(--ifm-heading-line-height);padding:var(--ifm-global-spacing);transition:border-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination-nav__link:hover{border-color:var(--ifm-pagination-nav-color-hover);text-decoration:none}.pagination-nav__link--next{grid-column:2/3;text-align:right}.pagination-nav__label{font-size:var(--ifm-h4-font-size);font-weight:var(--ifm-heading-font-weight);word-break:break-word}.pagination-nav__link--prev .pagination-nav__label:before{content:"« "}.pagination-nav__link--next .pagination-nav__label:after{content:" »"}.pagination-nav__sublabel{color:var(--ifm-color-content-secondary);font-size:var(--ifm-h5-font-size);font-weight:var(--ifm-font-weight-semibold);margin-bottom:.25rem}.pills__item,.tabs{font-weight:var(--ifm-font-weight-bold)}.admonitionContent_BuS1>:last-child,.bc-chat-popup p,.cardContainer_fWXF :last-child,.collapsibleContent_i85q p:last-child,.details_lb9f>summary>p:last-child,.tabItem_Ymn6>:last-child,.tabs,.terminalWindowBody_tzdS :last-child{margin-bottom:0}.pills{display:flex;gap:var(--ifm-pills-spacing);padding-left:0}.pills__item{border-radius:.5rem;cursor:pointer;display:inline-block;padding:.25rem 1rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs,:not(.containsTaskList_mC6p>li)>.containsTaskList_mC6p{padding-left:0}.pills__item--active{color:var(--ifm-pills-color-active)}.pills__item--active,.pills__item:not(.pills__item--active):hover{background:var(--ifm-pills-color-background-active)}.app-cta,.bc-layout,.bc-thumbs-button{background-color:#fff}.pills--block{justify-content:stretch}.pills--block .pills__item{flex-grow:1;text-align:center}.tabs{color:var(--ifm-tabs-color);display:flex;overflow-x:auto}.bc-input,.footer-cr a{font-weight:700}.tabs__item{border-bottom:3px solid #0000;border-radius:var(--ifm-global-radius);cursor:pointer;display:inline-flex;padding:var(--ifm-tabs-padding-vertical) var(--ifm-tabs-padding-horizontal);transition:background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs__item--active{border-bottom-color:var(--ifm-tabs-color-active-border);border-bottom-left-radius:0;border-bottom-right-radius:0;color:var(--ifm-tabs-color-active)}.tabs__item:hover{background-color:var(--ifm-hover-overlay)}.tabs--block{justify-content:stretch}.tabs--block .tabs__item{flex-grow:1;justify-content:center}.hp-list,.hp-section{flex-direction:column;display:flex}html[data-theme=dark]{--ifm-color-scheme:dark;--ifm-color-emphasis-0:var(--ifm-color-gray-1000);--ifm-color-emphasis-100:var(--ifm-color-gray-900);--ifm-color-emphasis-200:var(--ifm-color-gray-800);--ifm-color-emphasis-300:var(--ifm-color-gray-700);--ifm-color-emphasis-400:var(--ifm-color-gray-600);--ifm-color-emphasis-600:var(--ifm-color-gray-400);--ifm-color-emphasis-700:var(--ifm-color-gray-300);--ifm-color-emphasis-800:var(--ifm-color-gray-200);--ifm-color-emphasis-900:var(--ifm-color-gray-100);--ifm-color-emphasis-1000:var(--ifm-color-gray-0);--ifm-background-color:#1b1b1d;--ifm-background-surface-color:#242526;--ifm-hover-overlay:#ffffff0d;--ifm-color-content:#e3e3e3;--ifm-color-content-secondary:#fff;--ifm-breadcrumb-separator-filter:invert(64%) sepia(11%) saturate(0%) hue-rotate(149deg) brightness(99%) contrast(95%);--ifm-code-background:#ffffff1a;--ifm-scrollbar-track-background-color:#444;--ifm-scrollbar-thumb-background-color:#686868;--ifm-scrollbar-thumb-hover-background-color:#7a7a7a;--ifm-table-stripe-background:#ffffff12;--ifm-toc-border-color:var(--ifm-color-emphasis-200);--ifm-color-primary-contrast-background:#102445;--ifm-color-primary-contrast-foreground:#ebf2fc;--ifm-color-secondary-contrast-background:#474748;--ifm-color-secondary-contrast-foreground:#fdfdfe;--ifm-color-success-contrast-background:#003100;--ifm-color-success-contrast-foreground:#e6f6e6;--ifm-color-info-contrast-background:#193c47;--ifm-color-info-contrast-foreground:#eef9fd;--ifm-color-warning-contrast-background:#4d3800;--ifm-color-warning-contrast-foreground:#fff8e6;--ifm-color-danger-contrast-background:#4b1113;--ifm-color-danger-contrast-foreground:#ffebec;--docsearch-text-color:#f5f6f7;--docsearch-container-background:#090a11cc;--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 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 #494c6a80,0 -4px 8px 0 #0003;--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}#nprogress .bar{background:var(--docusaurus-progress-bar-color);height:2px;left:0;position:fixed;top:0;width:100%;z-index:1031}#nprogress .peg{box-shadow:0 0 10px var(--docusaurus-progress-bar-color),0 0 5px var(--docusaurus-progress-bar-color);height:100%;opacity:1;position:absolute;right:0;transform:rotate(3deg) translateY(-4px);width:100px}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:normal;font-weight:400;src:url(/assets/fonts/iAWriterQuattroSRegular-e47bd9556e7465f3cdca8944aebc1681.ttf) format("truetype")}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:normal;font-weight:700;src:url(/assets/fonts/iAWriterQuattroSBold-51c10554a8c6ab1d1e55af1528555325.ttf) format("truetype")}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:italic;font-weight:400;src:url(/assets/fonts/iAWriterQuattroSItalic-7584f678988e0674d9c5222d55b51786.ttf) format("truetype")}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:italic;font-weight:700;src:url(/assets/fonts/iAWriterQuattroSBoldItalic-6650b8cf6508d3ef099ecad91287ff48.ttf) format("truetype")}.hp-layout{margin:0 auto;max-width:42rem}.hp-hero{margin-top:3rem;width:18rem}.hp-header{font-size:1.875rem;line-height:2.25rem;margin-bottom:3.5rem;margin-top:6rem}.hp-list{gap:1rem;margin-left:-1rem}.hp-listitem{color:#00f;list-style-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTQuNDQzIDExLjI5M2EuOTQ5Ljk0OSAwIDAgMCAuODQxLS40NDRsNi4wNy05LjQxM2MuMDY5LS4xMS4xMTktLjIxNS4xNS0uMzE1YS45NDcuOTQ3IDAgMCAwIC4wNDktLjI4Ny44MTQuODE0IDAgMCAwLS4yMzMtLjYwMkEuODE0LjgxNCAwIDAgMCAxMC43MiAwYS44NjQuODY0IDAgMCAwLS40MzguMTAzIDEuMDYxIDEuMDYxIDAgMCAwLS4zMjguMzQ4TDQuNDE2IDkuMjM1IDEuNTcyIDUuNjEyYy0uMTkxLS4yNS0uNDI4LS4zNzYtLjcxLS4zNzYtLjI1MSAwLS40NTkuMDgtLjYyMy4yNC0uMTYuMTYtLjIzOS4zNi0uMjM5LjYwMSAwIC4xMS4wMi4yMTcuMDYyLjMyMS4wNC4xLjEwNy4yMDYuMTk4LjMxNWwzLjM0MyA0LjE1NmMuMjMyLjI4My41MTIuNDI0Ljg0LjQyNFoiIGZpbGw9IiMwMEYiLz48L3N2Zz4=)}.hp-listitem,.hp-text{font-size:1.25rem;line-height:1.75rem}.hp-text{margin-top:5rem}.hp-section{margin-top:9rem}.hp-section *{font-size:1.25rem}.hp-section-header{font-size:1.125rem;line-height:1.75rem;margin-bottom:1rem;text-decoration:underline}.cta-text{color:#00f}.footer-container{display:flex;flex-direction:column;font-size:1.25rem;gap:8rem;margin:0 auto;padding:4rem 0;width:70%}.footer-ls{display:flex;flex-wrap:wrap;gap:5rem 2rem;justify-content:space-around}.footer-ls-section{display:flex;flex-direction:column;gap:1.5rem}.footer-ls-section-title{color:#000;font-size:1.25rem;text-decoration-line:underline}.footer-ls-column{gap:1rem}.footer-cr,.footer-ls-column{display:flex;flex-direction:column}.footer-cr{align-items:center;gap:5rem;justify-content:center}.footer-cr p{text-align:center}.app-cta{border:2px solid #00f;border-radius:0;color:#00f;cursor:pointer;font-size:18px;padding:9px 15px;text-decoration:none}*,:root{--ifm-link-color:#00f;--ifm-color-primary:#00f;--ifm-color-primary-dark:#0000e6;--ifm-color-primary-darker:#0000d9;--ifm-color-primary-darkest:#0000b3;--ifm-color-primary-light:#1a1aff;--ifm-color-primary-lighter:#2626ff;--ifm-color-primary-lightest:#4d4dff;--privategpt-color:#5c00e2;--privategpt-width:56rem;--privategpt-max-width:100%;--privategpt-color-lightest:#f1e8fe}.bc-layout{min-height:calc(100vh - 60px);padding-bottom:6rem;padding-top:4rem}.bc-layout>*{transition:.5s}.bc-searchbar-icon{flex-shrink:0;height:22px;width:22.25px}.bc-thumbs-container{display:flex;margin-bottom:2rem}.bc-thumbs-button{align-items:center;border:1px solid var(--privategpt-color);border-radius:50%;display:flex;height:40px;justify-content:center;margin:.3rem;width:40px}.bc-quick-question:hover,.bc-thumbs-button:disabled,.bc-thumbs-button:hover{background-color:var(--privategpt-color-lightest)}.bc-thumbs-icon{border-radius:50%;height:30px;width:30px}.--loading,.bc-chat-embedded,.bc-quick-questions-panel,.bc-searchbar{display:flex;max-width:var(--privategpt-max-width);width:var(--privategpt-width)}.bc-searchbar{border-color:var(--privategpt-color);border-radius:50px;border-style:solid;border-width:1px;gap:1rem;padding:1rem;position:relative}.bc-input{border:none;color:#000;flex:1;font-size:medium;outline:0}.bc-beta-disclaimer,.bc-loader,.bc-quick-question{color:var(--privategpt-color)}.bc-input:disabled{background-color:#fff}.bc-reset-button{background:none;border:none;flex-shrink:0}.bc-reset-icon{height:15px}.bc-input::placeholder{color:gray;font-weight:400}.bc-chat-embedded{background-color:#fff;border-radius:1rem;flex-direction:column;height:-moz-fit-content;height:fit-content;padding:1rem;position:relative}.bc-quick-questions-panel{flex-wrap:wrap;gap:20px;justify-content:center;padding:30px 0}.bc-quick-question{background-color:#fff;border-color:var(--privategpt-color);border-radius:50px;border-width:1px;font-size:.85rem;padding:6px 15px}.bc-chat:first-child{margin-top:var(--ifm-paragraph-margin-bottom)}.--loading{align-items:center;justify-content:center;padding-top:1.5rem}.bc-loader{animation:1s linear infinite alternate a;border-radius:50%;display:block;height:12px;margin:15px auto;position:relative;width:12px}.bc-beta-disclaimer,.bc-chat-popup{max-width:var(--privategpt-max-width);width:var(--privategpt-width)}@keyframes a{0%{box-shadow:-38px -12px,-14px 0,14px 0,38px 0}33%{box-shadow:-38px 0,-14px -12px,14px 0,38px 0}66%{box-shadow:-38px 0,-14px 0,14px -12px,38px 0}to{box-shadow:-38px 0,-14px 0,14px 0,38px -12px}}.bc-chat-popup{align-items:center;background-color:#fff;border:2px solid #2a27e2;border-radius:10px;bottom:-200rem;display:flex;justify-content:center;padding:20px 30px;transition:.5s ease-in-out}.bc-beta-disclaimer{font-size:.65rem;margin-top:8px;text-align:center}svg{display:unset}.container{margin:unset;max-width:unset}.navbar_custom_item--button{background:none;border:none;margin-right:1rem;padding:0}.navbar_custom_item--image{height:15px}body:not(.navigation-with-keyboard) :not(input):focus{outline:0}#__docusaurus-base-url-issue-banner-container,.docSidebarContainer_YfHR,.sidebarLogo_isFc,.themedComponent_mlkZ,[data-theme=dark] .lightToggleIcon_pyhR,[data-theme=light] .darkToggleIcon_wfgR,html[data-announcement-bar-initially-dismissed=true] .announcementBar_mb4j{display:none}.skipToContent_fXgn{background-color:var(--ifm-background-surface-color);color:var(--ifm-color-emphasis-900);left:100%;padding:calc(var(--ifm-global-spacing)/2) var(--ifm-global-spacing);position:fixed;top:1rem;z-index:calc(var(--ifm-z-index-fixed) + 1)}.skipToContent_fXgn:focus{box-shadow:var(--ifm-global-shadow-md);left:1rem}.closeButton_CVFx{line-height:0;padding:0}.content_knG7{font-size:85%;padding:5px 0;text-align:center}.content_knG7 a{color:inherit;text-decoration:underline}.DocSearch-Container a,.sidebarItemLink_mo7H:hover{text-decoration:none}.announcementBar_mb4j{align-items:center;background-color:var(--ifm-color-white);border-bottom:1px solid var(--ifm-color-emphasis-100);color:var(--ifm-color-black);display:flex;height:var(--docusaurus-announcement-bar-height)}.terminalWindowHeader_o9Cs,.toggleButton_gllP:hover{background:var(--ifm-color-emphasis-200)}.announcementBarPlaceholder_vyr4{flex:0 0 10px}.announcementBarClose_gvF7{align-self:stretch;flex:0 0 30px}.announcementBarContent_xLdY{flex:1 1 auto}.toggle_vylO{height:2rem;width:2rem}.toggleButton_gllP{align-items:center;border-radius:50%;display:flex;height:100%;justify-content:center;transition:background var(--ifm-transition-fast);width:100%}.toggleButtonDisabled_aARS{cursor:not-allowed}.darkNavbarColorModeToggle_X3D1:hover{background:var(--ifm-color-gray-800)}[data-theme=dark] .themedComponent--dark_xIcU,[data-theme=light] .themedComponent--light_NVdE,html:not([data-theme]) .themedComponent--light_NVdE{display:initial}.iconExternalLink_nPIU{margin-left:.3rem}.dropdownNavbarItemMobile_S0Fm{cursor:pointer}.iconLanguage_nlXk{margin-right:5px;vertical-align:text-bottom}@supports selector(:has(*)){.navbarSearchContainer_Bca1:not(:has(>*)){display:none}}.navbarHideable_m1mJ{transition:transform var(--ifm-transition-fast) ease}.navbarHidden_jGov{transform:translate3d(0,calc(-100% - 2px),0)}.errorBoundaryError_a6uf{color:red;white-space:pre-wrap}.errorBoundaryFallback_VBag{color:red;padding:.55rem}.anchorWithStickyNavbar_LWe7{scroll-margin-top:calc(var(--ifm-navbar-height) + .5rem)}.anchorWithHideOnScrollNavbar_WYt5{scroll-margin-top:.5rem}.hash-link{opacity:0;padding-left:.5rem;transition:opacity var(--ifm-transition-fast);-webkit-user-select:none;user-select:none}.hash-link:before{content:"#"}.hash-link:focus,:hover>.hash-link{opacity:1}.mainWrapper_z2l0{display:flex;flex:1 0 auto;flex-direction:column}.docusaurus-mt-lg{margin-top:3rem}#__docusaurus{display:flex;flex-direction:column;min-height:100%}.terminalWindow_wGrl{border:3px solid var(--ifm-color-emphasis-200);border-radius:var(--ifm-global-radius);box-shadow:var(--ifm-global-shadow-lw);margin-bottom:var(--ifm-leading)}.terminalWindowHeader_o9Cs{align-items:center;display:flex;padding:.5rem 1rem}.row_Rn7G:after{clear:both;content:"";display:table}.buttons_IGLB{white-space:nowrap}.right_fWp9{align-self:center;width:10%}.terminalWindowAddressBar_X8fO{background-color:var(--ifm-background-color);border-radius:12.5px;color:var(--ifm-color-gray-800);flex:1 0;font:400 13px Arial,sans-serif;margin:0 1rem 0 .5rem;padding:5px 15px;-webkit-user-select:none;user-select:none}[data-theme=dark] .terminalWindowAddressBar_X8fO{color:var(--ifm-color-gray-300)}.dot_fGZE{background-color:#bbb;border-radius:50%;display:inline-block;height:12px;margin-right:6px;margin-top:4px;width:12px}.terminalWindowMenuIcon_rtOE{margin-left:auto}.bar_Ck8N{background-color:#aaa;display:block;height:3px;margin:3px 0;width:17px}.terminalWindowBody_tzdS{background-color:#292d3e;border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;color:#bfc7d5;padding:1rem}.terminalWindowBody_tzdS *{color:#bfc7d5}.cardContainer_fWXF{--ifm-link-color:var(--ifm-color-emphasis-800);--ifm-link-hover-color:var(--ifm-color-emphasis-700);--ifm-link-hover-decoration:none;border:1px solid var(--ifm-color-emphasis-200);box-shadow:0 1.5px 3px 0 #00000026;transition:all var(--ifm-transition-fast) ease;transition-property:border,box-shadow}.cardContainer_fWXF:hover{border-color:var(--ifm-color-primary);box-shadow:0 3px 6px 0 #0003}.cardTitle_rnsV{font-size:1.2rem}.cardDescription_PWke{font-size:.8rem}.searchQueryInput_u2C7,.searchVersionInput_m0Ui{background:var(--docsearch-searchbox-focus-background);border:2px solid var(--ifm-toc-border-color);border-radius:var(--ifm-global-radius);color:var(--docsearch-text-color);font:var(--ifm-font-size-base) var(--ifm-font-family-base);margin-bottom:.5rem;padding:.8rem;transition:border var(--ifm-transition-fast) ease;width:100%}.searchQueryInput_u2C7:focus,.searchVersionInput_m0Ui:focus{border-color:var(--docsearch-primary-color);outline:0}.searchQueryInput_u2C7::placeholder{color:var(--docsearch-muted-color)}.searchResultsColumn_JPFH{font-size:.9rem;font-weight:700}.algoliaLogo_rT1R{max-width:150px}.algoliaLogoPathFill_WdUC{fill:var(--ifm-font-color-base)}.searchResultItem_Tv2o{border-bottom:1px solid var(--ifm-toc-border-color);padding:1rem 0}.searchResultItemHeading_KbCB{font-weight:400;margin-bottom:0}.searchResultItemPath_lhe1{--ifm-breadcrumb-separator-size-multiplier:1;color:var(--ifm-color-content-secondary);font-size:.8rem}.searchResultItemSummary_AEaO{font-style:italic;margin:.5rem 0 0}.loadingSpinner_XVxU{animation:1s linear infinite b;border:.4em solid #eee;border-radius:50%;border-top:.4em solid var(--ifm-color-primary);height:3rem;margin:0 auto;width:3rem}@keyframes b{to{transform:rotate(1turn)}}.loader_vvXV{margin-top:2rem}.search-result-match{background:#ffd78e40;color:var(--docsearch-hit-color);padding:.09em 0}.tabList__CuJ{margin-bottom:var(--ifm-leading)}.tabItem_LNqP{margin-top:0!important}.sidebar_re4s{max-height:calc(100vh - var(--ifm-navbar-height) - 2rem);overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 2rem)}.sidebarItemTitle_pO2u{font-size:var(--ifm-h3-font-size);font-weight:var(--ifm-font-weight-bold)}.container_mt6G,.sidebarItemList_Yudw{font-size:.9rem}.sidebarItem__DBe{margin-top:.7rem}.sidebarItemLink_mo7H{color:var(--ifm-font-color-base);display:block}.sidebarItemLinkActive_I1ZP{color:var(--ifm-color-primary)!important}.title_f1Hy{font-size:3rem}.authorCol_Hf19{flex-grow:1!important;max-width:inherit!important}.imageOnlyAuthorRow_pa_O{display:flex;flex-flow:row wrap}.imageOnlyAuthorCol_G86a{margin-left:.3rem;margin-right:.3rem}.backToTopButton_sjWU{background-color:var(--ifm-color-emphasis-200);border-radius:50%;bottom:1.3rem;box-shadow:var(--ifm-global-shadow-lw);height:3rem;opacity:0;position:fixed;right:1.3rem;transform:scale(0);transition:all var(--ifm-transition-fast) var(--ifm-transition-timing-default);visibility:hidden;width:3rem;z-index:calc(var(--ifm-z-index-fixed) - 1)}.backToTopButton_sjWU:after{background-color:var(--ifm-color-emphasis-1000);content:" ";display:inline-block;height:100%;-webkit-mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;width:100%}.backToTopButtonShow_xfvO{opacity:1;transform:scale(1);visibility:visible}[data-theme=dark]:root{--docusaurus-collapse-button-bg:#ffffff0d;--docusaurus-collapse-button-bg-hover:#ffffff1a}.collapseSidebarButton_PEFL{display:none;margin:0}.menuExternalLink_NmtK{align-items:center}.docMainContainer_TBSr,.docRoot_UBD9{display:flex;width:100%}.docsWrapper_hBAB{display:flex;flex:1 0 auto}.DocSearch-Button,.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Button{background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;font-weight:500;height:36px;justify-content:space-between;padding:0 8px;-webkit-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:0}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Hit-Tree,.DocSearch-Hit-action,.DocSearch-Hit-icon,.DocSearch-Reset{stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Input,.DocSearch-Link{-webkit-appearance:none;font:inherit}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch--active{overflow:hidden!important}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Link{appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;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{appearance:none;background:#0000;border:0;color:var(--docsearch-text-color);flex:1;font-size:1.2em;height:100%;outline:0;padding:0 0 0 8px;width:80%}.DocSearch-Hit-action-button,.DocSearch-Reset{-webkit-appearance:none;border:0;cursor:pointer}.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-Container--Stalled .DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Cancel,.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator,.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset{animation:.1s ease-in forwards c;appearance:none;background:none;border-radius:50%;color:var(--docsearch-icon-color);padding:2px;right:0}.DocSearch-Help,.DocSearch-HitsFooter,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.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:#0000}.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{font-size:.9em;margin:0;-webkit-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{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}.DocSearch-Hit--deleting{opacity:0;transition:.25s linear}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:.25s linear .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;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)}.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{appearance:none;background:none;border-radius:50%;color:inherit;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon,.tocCollapsibleContent_vkbj a{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}.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-Tree,.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] mark{color:var(--docsearch-hit-active-color)!important}.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:"» "}.DocSearch-Prefill{-webkit-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:0;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;user-select:none;width:100%;z-index:300}.DocSearch-Commands li,.DocSearch-Commands-Key{align-items:center;display:flex}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.buttonGroup__atx button,.codeBlockContainer_Ckt0{background:var(--prism-background-color);color:var(--prism-color)}@keyframes c{0%{opacity:0}to{opacity:1}}.DocSearch-Button{margin:0;transition:all var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.DocSearch-Container{z-index:calc(var(--ifm-z-index-fixed) + 1)}.codeBlockContainer_Ckt0{border-radius:var(--ifm-code-border-radius);box-shadow:var(--ifm-global-shadow-lw);margin-bottom:var(--ifm-leading)}.codeBlockContent_biex{border-radius:inherit;direction:ltr;position:relative}.codeBlockTitle_Ktv7{border-bottom:1px solid var(--ifm-color-emphasis-300);border-top-left-radius:inherit;border-top-right-radius:inherit;font-size:var(--ifm-code-font-size);font-weight:500;padding:.75rem var(--ifm-pre-padding)}.codeBlock_bY9V{--ifm-pre-background:var(--prism-background-color);margin:0;padding:0}.codeBlockTitle_Ktv7+.codeBlockContent_biex .codeBlock_bY9V{border-top-left-radius:0;border-top-right-radius:0}.codeBlockStandalone_MEMb{padding:0}.codeBlockLines_e6Vv{float:left;font:inherit;min-width:100%;padding:var(--ifm-pre-padding)}.codeBlockLinesWithNumbering_o6Pm{display:table;padding:var(--ifm-pre-padding) 0}.buttonGroup__atx{column-gap:.2rem;display:flex;position:absolute;right:calc(var(--ifm-pre-padding)/2);top:calc(var(--ifm-pre-padding)/2)}.buttonGroup__atx button{align-items:center;border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-global-radius);display:flex;line-height:0;opacity:0;padding:.4rem;transition:opacity var(--ifm-transition-fast) ease-in-out}.buttonGroup__atx button:focus-visible,.buttonGroup__atx button:hover{opacity:1!important}.theme-code-block:hover .buttonGroup__atx button{opacity:.4}.iconEdit_Z9Sw{margin-right:.3em;vertical-align:sub}:where(:root){--docusaurus-highlighted-code-line-bg:#484d5b}:where([data-theme=dark]){--docusaurus-highlighted-code-line-bg:#646464}.theme-code-block-highlighted-line{background-color:var(--docusaurus-highlighted-code-line-bg);display:block;margin:0 calc(var(--ifm-pre-padding)*-1);padding:0 var(--ifm-pre-padding)}.codeLine_lJS_{counter-increment:a;display:table-row}.codeLineNumber_Tfdd{background:var(--ifm-pre-background);display:table-cell;left:0;overflow-wrap:normal;padding:0 var(--ifm-pre-padding);position:sticky;text-align:right;width:1%}.codeLineNumber_Tfdd:before{content:counter(a);opacity:.4}.theme-code-block-highlighted-line .codeLineNumber_Tfdd:before{opacity:.8}.codeLineContent_feaV{padding-right:var(--ifm-pre-padding)}.tag_zVej{border:1px solid var(--docusaurus-tag-list-border);transition:border var(--ifm-transition-fast)}.tag_zVej:hover{--docusaurus-tag-list-border:var(--ifm-link-color);text-decoration:none}.tagRegular_sFm0{border-radius:var(--ifm-global-radius);font-size:90%;padding:.2rem .5rem .3rem}.tagWithCount_h2kH{align-items:center;border-left:0;display:flex;padding:0 .5rem 0 1rem;position:relative}.tagWithCount_h2kH:after,.tagWithCount_h2kH:before{border:1px solid var(--docusaurus-tag-list-border);content:"";position:absolute;top:50%;transition:inherit}.tagWithCount_h2kH:before{border-bottom:0;border-right:0;height:1.18rem;right:100%;transform:translate(50%,-50%) rotate(-45deg);width:1.18rem}.tagWithCount_h2kH:after{border-radius:50%;height:.5rem;left:0;transform:translateY(-50%);width:.5rem}.tagWithCount_h2kH span{background:var(--ifm-color-secondary);border-radius:var(--ifm-global-radius);color:var(--ifm-color-black);font-size:.7rem;line-height:1.2;margin-left:.3rem;padding:.1rem .4rem}.theme-code-block:hover .copyButtonCopied_obH4{opacity:1!important}.copyButtonIcons_eSgA{height:1.125rem;position:relative;width:1.125rem}.copyButtonIcon_y97N,.copyButtonSuccessIcon_LjdS{fill:currentColor;height:inherit;left:0;opacity:inherit;position:absolute;top:0;transition:all var(--ifm-transition-fast) ease;width:inherit}.copyButtonSuccessIcon_LjdS{color:#00d600;left:50%;opacity:0;top:50%;transform:translate(-50%,-50%) scale(.33)}.copyButtonCopied_obH4 .copyButtonIcon_y97N{opacity:0;transform:scale(.33)}.copyButtonCopied_obH4 .copyButtonSuccessIcon_LjdS{opacity:1;transform:translate(-50%,-50%) scale(1);transition-delay:75ms}.tags_jXut{display:inline}.tag_QGVx{display:inline-block;margin:0 .4rem .5rem 0}.lastUpdated_vwxv{font-size:smaller;font-style:italic;margin-top:.2rem}.tocCollapsibleButton_TO0P{align-items:center;display:flex;font-size:inherit;justify-content:space-between;padding:.4rem .8rem;width:100%}.tocCollapsibleButton_TO0P:after{background:var(--ifm-menu-link-sublist-icon) 50% 50%/2rem 2rem no-repeat;content:"";filter:var(--ifm-menu-link-sublist-icon-filter);height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast);width:1.25rem}.tocCollapsibleButtonExpanded_MG3E:after,.tocCollapsibleExpanded_sAul{transform:none}.tocCollapsible_ETCw{background-color:var(--ifm-menu-color-background-active);border-radius:var(--ifm-global-radius);margin:1rem 0}.tocCollapsibleContent_vkbj>ul{border-left:none;border-top:1px solid var(--ifm-color-emphasis-300);font-size:15px;padding:.2rem 0}.tocCollapsibleContent_vkbj ul li{margin:.4rem .8rem}.wordWrapButtonIcon_Bwma{height:1.2rem;width:1.2rem}.wordWrapButtonEnabled_EoeP .wordWrapButtonIcon_Bwma{color:var(--ifm-color-primary)}.details_lb9f{--docusaurus-details-summary-arrow-size:0.38rem;--docusaurus-details-transition:transform 200ms ease;--docusaurus-details-decoration-color:grey}.details_lb9f>summary{cursor:pointer;list-style:none;padding-left:1rem;position:relative}.details_lb9f>summary::-webkit-details-marker{display:none}.details_lb9f>summary:before{border-color:#0000 #0000 #0000 var(--docusaurus-details-decoration-color);border-style:solid;border-width:var(--docusaurus-details-summary-arrow-size);content:"";left:0;position:absolute;top:.45rem;transform:rotate(0);transform-origin:calc(var(--docusaurus-details-summary-arrow-size)/2) 50%;transition:var(--docusaurus-details-transition)}.collapsibleContent_i85q{border-top:1px solid var(--docusaurus-details-decoration-color);margin-top:1rem;padding-top:1rem}.details_b_Ee{--docusaurus-details-decoration-color:var(--ifm-alert-border-color);--docusaurus-details-transition:transform var(--ifm-transition-fast) ease;border:1px solid var(--ifm-alert-border-color);margin:0 0 var(--ifm-spacing-vertical)}.containsTaskList_mC6p{list-style:none}.img_ev3q{height:auto}.admonition_xJq3{margin-bottom:1em}.admonitionHeading_Gvgb{font:var(--ifm-heading-font-weight) var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family)}.admonitionHeading_Gvgb:not(:last-child){margin-bottom:.3rem}.admonitionHeading_Gvgb code{text-transform:none}.admonitionIcon_Rf37{display:inline-block;margin-right:.4em;vertical-align:middle}.admonitionIcon_Rf37 svg{fill:var(--ifm-alert-foreground-color);display:inline-block;height:1.6em;width:1.6em}.blogPostFooterDetailsFull_mRVl{flex-direction:column}.tableOfContents_bqdL{max-height:calc(100vh - var(--ifm-navbar-height) - 2rem);overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 1rem)}.breadcrumbHomeIcon_YNFT{height:1.1rem;position:relative;top:1px;vertical-align:top;width:1.1rem}.breadcrumbsContainer_Z_bl{--ifm-breadcrumb-size-multiplier:0.8;margin-bottom:.8rem}.title_kItE{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-leading)*1.25)}.list_eTzJ article:last-child{margin-bottom:0!important}@media screen and (min-width:769px) and (max-width:997px){.navbar_custom_item--button{margin-right:11rem}}@media (min-width:997px){.collapseSidebarButton_PEFL,.expandButton_TmdG{background-color:var(--docusaurus-collapse-button-bg)}:root{--docusaurus-announcement-bar-height:30px}.announcementBarClose_gvF7,.announcementBarPlaceholder_vyr4{flex-basis:50px}.navbarSearchContainer_Bca1{padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.collapseSidebarButton_PEFL{border:1px solid var(--ifm-toc-border-color);border-radius:0;bottom:0;display:block!important;height:40px;position:sticky}.collapseSidebarButtonIcon_kv0_{margin-top:4px;transform:rotate(180deg)}.expandButtonIcon_i1dp,[dir=rtl] .collapseSidebarButtonIcon_kv0_{transform:rotate(0)}.collapseSidebarButton_PEFL:focus,.collapseSidebarButton_PEFL:hover,.expandButton_TmdG:focus,.expandButton_TmdG:hover{background-color:var(--docusaurus-collapse-button-bg-hover)}.menuHtmlItem_M9Kj{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu_SIkG{flex-grow:1;padding:.5rem}@supports (scrollbar-gutter:stable){.menu_SIkG{padding:.5rem 0 .5rem .5rem;scrollbar-gutter:stable}}.menuWithAnnouncementBar_GW3s{margin-bottom:var(--docusaurus-announcement-bar-height)}.sidebar_njMd{display:flex;flex-direction:column;height:100%;padding-top:var(--ifm-navbar-height);width:var(--doc-sidebar-width)}.sidebarWithHideableNavbar_wUlq{padding-top:0}.sidebarHidden_VK0M{opacity:0;visibility:hidden}.sidebarLogo_isFc{align-items:center;color:inherit!important;display:flex!important;margin:0 var(--ifm-navbar-padding-horizontal);max-height:var(--ifm-navbar-height);min-height:var(--ifm-navbar-height);text-decoration:none!important}.sidebarLogo_isFc img{height:2rem;margin-right:.5rem}.expandButton_TmdG{align-items:center;display:flex;height:100%;justify-content:center;position:absolute;right:0;top:0;transition:background-color var(--ifm-transition-fast) ease;width:100%}[dir=rtl] .expandButtonIcon_i1dp{transform:rotate(180deg)}.docSidebarContainer_YfHR{border-right:1px solid var(--ifm-toc-border-color);-webkit-clip-path:inset(0);clip-path:inset(0);display:block;margin-top:calc(var(--ifm-navbar-height)*-1);transition:width var(--ifm-transition-fast) ease;width:var(--doc-sidebar-width);will-change:width}.docSidebarContainerHidden_DPk8{cursor:pointer;width:var(--doc-sidebar-hidden-width)}.sidebarViewport_aRkj{height:100%;max-height:100vh;position:sticky;top:0}.docMainContainer_TBSr{flex-grow:1;max-width:calc(100% - var(--doc-sidebar-width))}.docMainContainerEnhanced_lQrH{max-width:calc(100% - var(--doc-sidebar-hidden-width))}.docItemWrapperEnhanced_JWYK{max-width:calc(var(--ifm-container-width) + var(--doc-sidebar-width))!important}.lastUpdated_vwxv{text-align:right}.tocMobile_ITEo{display:none}.docItemCol_VOVn,.generatedIndexPage_vN6x{max-width:75%!important}.list_eTzJ article:nth-last-child(-n+2){margin-bottom:0!important}}@media screen and (min-width:1280px){.footer-container{width:55%}}@media (min-width:1440px){.container{max-width:var(--ifm-container-width-xl)}}@media (max-width:996px){.col{--ifm-col-width:100%;flex-basis:var(--ifm-col-width);margin-left:0}.footer{--ifm-footer-padding-horizontal:0}.colorModeToggle_DEke,.footer__link-separator,.navbar__item,.sidebar_re4s,.tableOfContents_bqdL{display:none}.footer__col{margin-bottom:calc(var(--ifm-spacing-vertical)*3)}.footer__link-item{display:block}.hero{padding-left:0;padding-right:0}.navbar>.container,.navbar>.container-fluid{padding:0}.navbar__toggle{display:inherit}.navbar__search-input{width:9rem}.pills--block,.tabs--block{flex-direction:column}.navbarSearchContainer_Bca1{position:absolute;right:var(--ifm-navbar-padding-horizontal)}.docItemContainer_F8PC{padding:0 .3rem}}@media only screen and (max-width:996px){.searchQueryColumn_RTkw,.searchResultsColumn_JPFH{max-width:60%!important}.searchLogoColumn_rJIA,.searchVersionColumn_ypXd{max-width:40%!important}.searchLogoColumn_rJIA{padding-left:0!important}}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder,.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%;max-height:calc(var(--docsearch-vh,1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.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-Cancel{-webkit-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:0;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}}@media screen and (max-width:768px){.navbar_custom_item--button{margin-right:3rem}}@media screen and (max-width:767px){.hp-layout{max-width:90%}}@media (max-width:576px){.markdown h1:first-child{--ifm-h1-font-size:2rem}.markdown>h2{--ifm-h2-font-size:1.5rem}.markdown>h3{--ifm-h3-font-size:1.25rem}.title_f1Hy{font-size:2rem}}@media screen and (max-width:576px){.searchQueryColumn_RTkw{max-width:100%!important}.searchVersionColumn_ypXd{max-width:100%!important;padding-left:var(--ifm-spacing-horizontal)!important}}@media (hover:hover){.backToTopButton_sjWU:hover{background-color:var(--ifm-color-emphasis-300)}}@media (pointer:fine){.thin-scrollbar{scrollbar-width:thin}.thin-scrollbar::-webkit-scrollbar{height:var(--ifm-scrollbar-size);width:var(--ifm-scrollbar-size)}.thin-scrollbar::-webkit-scrollbar-track{background:var(--ifm-scrollbar-track-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb{background:var(--ifm-scrollbar-thumb-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb:hover{background:var(--ifm-scrollbar-thumb-hover-background-color)}}@media (prefers-reduced-motion:reduce){:root{--ifm-transition-fast:0ms;--ifm-transition-slow:0ms}}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{stroke-width:var(--docsearch-icon-stroke-width);animation:none;-webkit-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0}.DocSearch-Hit--deleting,.DocSearch-Hit--favoriting{transition:none}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}@media print{.announcementBar_mb4j,.footer,.menu,.navbar,.pagination-nav,.table-of-contents,.tocMobile_ITEo{display:none}.tabs{page-break-inside:avoid}.codeBlockLines_e6Vv{white-space:pre-wrap}} \ No newline at end of file +@import url(https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap);.col,.container{padding:0 var(--ifm-spacing-horizontal);width:100%}.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{margin-bottom:calc(var(--ifm-heading-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown li,body{word-wrap:break-word}body,ol ol,ol ul,ul ol,ul ul{margin:0}pre,table{overflow:auto}blockquote,pre{margin:0 0 var(--ifm-spacing-vertical)}.breadcrumbs__link,.button{transition-timing-function:var(--ifm-transition-timing-default)}.button,code{vertical-align:middle}.button--outline.button--active,.button--outline:active,.button--outline:hover,:root{--ifm-button-color:var(--ifm-font-color-base-inverse)}.menu__link:hover,a{transition:color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.navbar--dark,:root{--ifm-navbar-link-hover-color:var(--ifm-color-primary)}.menu,.navbar-sidebar{overflow-x:hidden}:root,html[data-theme=dark]{--ifm-color-emphasis-500:var(--ifm-color-gray-500)}.toggleButton_gllP,html{-webkit-tap-highlight-color:transparent}*,.DocSearch-Container,.DocSearch-Container *,.bc-loader{box-sizing:border-box}:root{--ifm-color-scheme:light;--ifm-dark-value:10%;--ifm-darker-value:15%;--ifm-darkest-value:30%;--ifm-light-value:15%;--ifm-lighter-value:30%;--ifm-lightest-value:50%;--ifm-contrast-background-value:90%;--ifm-contrast-foreground-value:70%;--ifm-contrast-background-dark-value:70%;--ifm-contrast-foreground-dark-value:90%;--ifm-color-primary:#3578e5;--ifm-color-secondary:#ebedf0;--ifm-color-success:#00a400;--ifm-color-info:#54c7ec;--ifm-color-warning:#ffba00;--ifm-color-danger:#fa383e;--ifm-color-primary-dark:#306cce;--ifm-color-primary-darker:#2d66c3;--ifm-color-primary-darkest:#2554a0;--ifm-color-primary-light:#538ce9;--ifm-color-primary-lighter:#72a1ed;--ifm-color-primary-lightest:#9abcf2;--ifm-color-primary-contrast-background:#ebf2fc;--ifm-color-primary-contrast-foreground:#102445;--ifm-color-secondary-dark:#d4d5d8;--ifm-color-secondary-darker:#c8c9cc;--ifm-color-secondary-darkest:#a4a6a8;--ifm-color-secondary-light:#eef0f2;--ifm-color-secondary-lighter:#f1f2f5;--ifm-color-secondary-lightest:#f5f6f8;--ifm-color-secondary-contrast-background:#fdfdfe;--ifm-color-secondary-contrast-foreground:#474748;--ifm-color-success-dark:#009400;--ifm-color-success-darker:#008b00;--ifm-color-success-darkest:#007300;--ifm-color-success-light:#26b226;--ifm-color-success-lighter:#4dbf4d;--ifm-color-success-lightest:#80d280;--ifm-color-success-contrast-background:#e6f6e6;--ifm-color-success-contrast-foreground:#003100;--ifm-color-info-dark:#4cb3d4;--ifm-color-info-darker:#47a9c9;--ifm-color-info-darkest:#3b8ba5;--ifm-color-info-light:#6ecfef;--ifm-color-info-lighter:#87d8f2;--ifm-color-info-lightest:#aae3f6;--ifm-color-info-contrast-background:#eef9fd;--ifm-color-info-contrast-foreground:#193c47;--ifm-color-warning-dark:#e6a700;--ifm-color-warning-darker:#d99e00;--ifm-color-warning-darkest:#b38200;--ifm-color-warning-light:#ffc426;--ifm-color-warning-lighter:#ffcf4d;--ifm-color-warning-lightest:#ffdd80;--ifm-color-warning-contrast-background:#fff8e6;--ifm-color-warning-contrast-foreground:#4d3800;--ifm-color-danger-dark:#e13238;--ifm-color-danger-darker:#d53035;--ifm-color-danger-darkest:#af272b;--ifm-color-danger-light:#fb565b;--ifm-color-danger-lighter:#fb7478;--ifm-color-danger-lightest:#fd9c9f;--ifm-color-danger-contrast-background:#ffebec;--ifm-color-danger-contrast-foreground:#4b1113;--ifm-color-white:#fff;--ifm-color-black:#000;--ifm-color-gray-0:var(--ifm-color-white);--ifm-color-gray-100:#f5f6f7;--ifm-color-gray-200:#ebedf0;--ifm-color-gray-300:#dadde1;--ifm-color-gray-400:#ccd0d5;--ifm-color-gray-500:#bec3c9;--ifm-color-gray-600:#8d949e;--ifm-color-gray-700:#606770;--ifm-color-gray-800:#444950;--ifm-color-gray-900:#1c1e21;--ifm-color-gray-1000:var(--ifm-color-black);--ifm-color-emphasis-0:var(--ifm-color-gray-0);--ifm-color-emphasis-100:var(--ifm-color-gray-100);--ifm-color-emphasis-200:var(--ifm-color-gray-200);--ifm-color-emphasis-300:var(--ifm-color-gray-300);--ifm-color-emphasis-400:var(--ifm-color-gray-400);--ifm-color-emphasis-600:var(--ifm-color-gray-600);--ifm-color-emphasis-700:var(--ifm-color-gray-700);--ifm-color-emphasis-800:var(--ifm-color-gray-800);--ifm-color-emphasis-900:var(--ifm-color-gray-900);--ifm-color-emphasis-1000:var(--ifm-color-gray-1000);--ifm-color-content:var(--ifm-color-emphasis-900);--ifm-color-content-inverse:var(--ifm-color-emphasis-0);--ifm-color-content-secondary:#525860;--ifm-background-color:#0000;--ifm-background-surface-color:var(--ifm-color-content-inverse);--ifm-global-border-width:1px;--ifm-global-radius:0.4rem;--ifm-hover-overlay:#0000000d;--ifm-font-color-base:var(--ifm-color-content);--ifm-font-color-base-inverse:var(--ifm-color-content-inverse);--ifm-font-color-secondary:var(--ifm-color-content-secondary);--ifm-font-family-base:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--ifm-font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ifm-font-size-base:100%;--ifm-font-weight-light:300;--ifm-font-weight-normal:400;--ifm-font-weight-semibold:500;--ifm-font-weight-bold:700;--ifm-font-weight-base:var(--ifm-font-weight-normal);--ifm-line-height-base:1.65;--ifm-global-spacing:1rem;--ifm-spacing-vertical:var(--ifm-global-spacing);--ifm-spacing-horizontal:var(--ifm-global-spacing);--ifm-transition-fast:200ms;--ifm-transition-slow:400ms;--ifm-transition-timing-default:cubic-bezier(0.08,0.52,0.52,1);--ifm-global-shadow-lw:0 1px 2px 0 #0000001a;--ifm-global-shadow-md:0 5px 40px #0003;--ifm-global-shadow-tl:0 12px 28px 0 #0003,0 2px 4px 0 #0000001a;--ifm-z-index-dropdown:100;--ifm-z-index-fixed:200;--ifm-z-index-overlay:400;--ifm-container-width:1140px;--ifm-container-width-xl:1320px;--ifm-code-background:#f6f7f8;--ifm-code-border-radius:var(--ifm-global-radius);--ifm-code-font-size:90%;--ifm-code-padding-horizontal:0.1rem;--ifm-code-padding-vertical:0.1rem;--ifm-pre-background:var(--ifm-code-background);--ifm-pre-border-radius:var(--ifm-code-border-radius);--ifm-pre-color:inherit;--ifm-pre-line-height:1.45;--ifm-pre-padding:1rem;--ifm-heading-color:inherit;--ifm-heading-margin-top:0;--ifm-heading-margin-bottom:var(--ifm-spacing-vertical);--ifm-heading-font-family:var(--ifm-font-family-base);--ifm-heading-font-weight:var(--ifm-font-weight-bold);--ifm-heading-line-height:1.25;--ifm-h1-font-size:2rem;--ifm-h2-font-size:1.5rem;--ifm-h3-font-size:1.25rem;--ifm-h4-font-size:1rem;--ifm-h5-font-size:0.875rem;--ifm-h6-font-size:0.85rem;--ifm-image-alignment-padding:1.25rem;--ifm-leading-desktop:1.25;--ifm-leading:calc(var(--ifm-leading-desktop)*1rem);--ifm-list-left-padding:2rem;--ifm-list-margin:1rem;--ifm-list-item-margin:0.25rem;--ifm-list-paragraph-margin:1rem;--ifm-table-cell-padding:0.75rem;--ifm-table-background:#0000;--ifm-table-stripe-background:#00000008;--ifm-table-border-width:1px;--ifm-table-border-color:var(--ifm-color-emphasis-300);--ifm-table-head-background:inherit;--ifm-table-head-color:inherit;--ifm-table-head-font-weight:var(--ifm-font-weight-bold);--ifm-table-cell-color:inherit;--ifm-link-color:var(--ifm-color-primary);--ifm-link-decoration:none;--ifm-link-hover-color:var(--ifm-link-color);--ifm-link-hover-decoration:underline;--ifm-paragraph-margin-bottom:var(--ifm-leading);--ifm-blockquote-font-size:var(--ifm-font-size-base);--ifm-blockquote-border-left-width:2px;--ifm-blockquote-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-blockquote-padding-vertical:0;--ifm-blockquote-shadow:none;--ifm-blockquote-color:var(--ifm-color-emphasis-800);--ifm-blockquote-border-color:var(--ifm-color-emphasis-300);--ifm-hr-background-color:var(--ifm-color-emphasis-500);--ifm-hr-height:1px;--ifm-hr-margin-vertical:1.5rem;--ifm-scrollbar-size:7px;--ifm-scrollbar-track-background-color:#f1f1f1;--ifm-scrollbar-thumb-background-color:silver;--ifm-scrollbar-thumb-hover-background-color:#a7a7a7;--ifm-alert-background-color:inherit;--ifm-alert-border-color:inherit;--ifm-alert-border-radius:var(--ifm-global-radius);--ifm-alert-border-width:0px;--ifm-alert-border-left-width:5px;--ifm-alert-color:var(--ifm-font-color-base);--ifm-alert-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-alert-padding-vertical:var(--ifm-spacing-vertical);--ifm-alert-shadow:var(--ifm-global-shadow-lw);--ifm-avatar-intro-margin:1rem;--ifm-avatar-intro-alignment:inherit;--ifm-avatar-photo-size:3rem;--ifm-badge-background-color:inherit;--ifm-badge-border-color:inherit;--ifm-badge-border-radius:var(--ifm-global-radius);--ifm-badge-border-width:var(--ifm-global-border-width);--ifm-badge-color:var(--ifm-color-white);--ifm-badge-padding-horizontal:calc(var(--ifm-spacing-horizontal)*0.5);--ifm-badge-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-breadcrumb-border-radius:1.5rem;--ifm-breadcrumb-spacing:0.5rem;--ifm-breadcrumb-color-active:var(--ifm-color-primary);--ifm-breadcrumb-item-background-active:var(--ifm-hover-overlay);--ifm-breadcrumb-padding-horizontal:0.8rem;--ifm-breadcrumb-padding-vertical:0.4rem;--ifm-breadcrumb-size-multiplier:1;--ifm-breadcrumb-separator:url('data:image/svg+xml;utf8,');--ifm-breadcrumb-separator-filter:none;--ifm-breadcrumb-separator-size:0.5rem;--ifm-breadcrumb-separator-size-multiplier:1.25;--ifm-button-background-color:inherit;--ifm-button-border-color:var(--ifm-button-background-color);--ifm-button-border-width:var(--ifm-global-border-width);--ifm-button-font-weight:var(--ifm-font-weight-bold);--ifm-button-padding-horizontal:1.5rem;--ifm-button-padding-vertical:0.375rem;--ifm-button-size-multiplier:1;--ifm-button-transition-duration:var(--ifm-transition-fast);--ifm-button-border-radius:calc(var(--ifm-global-radius)*var(--ifm-button-size-multiplier));--ifm-button-group-spacing:2px;--ifm-card-background-color:var(--ifm-background-surface-color);--ifm-card-border-radius:calc(var(--ifm-global-radius)*2);--ifm-card-horizontal-spacing:var(--ifm-global-spacing);--ifm-card-vertical-spacing:var(--ifm-global-spacing);--ifm-toc-border-color:var(--ifm-color-emphasis-300);--ifm-toc-link-color:var(--ifm-color-content-secondary);--ifm-toc-padding-vertical:0.5rem;--ifm-toc-padding-horizontal:0.5rem;--ifm-dropdown-background-color:var(--ifm-background-surface-color);--ifm-dropdown-font-weight:var(--ifm-font-weight-semibold);--ifm-dropdown-link-color:var(--ifm-font-color-base);--ifm-dropdown-hover-background-color:var(--ifm-hover-overlay);--ifm-footer-background-color:var(--ifm-color-emphasis-100);--ifm-footer-color:inherit;--ifm-footer-link-color:var(--ifm-color-emphasis-700);--ifm-footer-link-hover-color:var(--ifm-color-primary);--ifm-footer-link-horizontal-spacing:0.5rem;--ifm-footer-padding-horizontal:calc(var(--ifm-spacing-horizontal)*2);--ifm-footer-padding-vertical:calc(var(--ifm-spacing-vertical)*2);--ifm-footer-title-color:inherit;--ifm-footer-logo-max-width:min(30rem,90vw);--ifm-hero-background-color:var(--ifm-background-surface-color);--ifm-hero-text-color:var(--ifm-color-emphasis-800);--ifm-menu-color:var(--ifm-color-emphasis-700);--ifm-menu-color-active:var(--ifm-color-primary);--ifm-menu-color-background-active:var(--ifm-hover-overlay);--ifm-menu-color-background-hover:var(--ifm-hover-overlay);--ifm-menu-link-padding-horizontal:0.75rem;--ifm-menu-link-padding-vertical:0.375rem;--ifm-menu-link-sublist-icon:url('data:image/svg+xml;utf8,');--ifm-menu-link-sublist-icon-filter:none;--ifm-navbar-background-color:var(--ifm-background-surface-color);--ifm-navbar-height:3.75rem;--ifm-navbar-item-padding-horizontal:0.75rem;--ifm-navbar-item-padding-vertical:0.25rem;--ifm-navbar-link-color:var(--ifm-font-color-base);--ifm-navbar-link-active-color:var(--ifm-link-color);--ifm-navbar-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-navbar-padding-vertical:calc(var(--ifm-spacing-vertical)*0.5);--ifm-navbar-shadow:var(--ifm-global-shadow-lw);--ifm-navbar-search-input-background-color:var(--ifm-color-emphasis-200);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-800);--ifm-navbar-search-input-placeholder-color:var(--ifm-color-emphasis-500);--ifm-navbar-search-input-icon:url('data:image/svg+xml;utf8,');--ifm-navbar-sidebar-width:83vw;--ifm-pagination-border-radius:var(--ifm-global-radius);--ifm-pagination-color-active:var(--ifm-color-primary);--ifm-pagination-font-size:1rem;--ifm-pagination-item-active-background:var(--ifm-hover-overlay);--ifm-pagination-page-spacing:0.2em;--ifm-pagination-padding-horizontal:calc(var(--ifm-spacing-horizontal)*1);--ifm-pagination-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-pagination-nav-border-radius:var(--ifm-global-radius);--ifm-pagination-nav-color-hover:var(--ifm-color-primary);--ifm-pills-color-active:var(--ifm-color-primary);--ifm-pills-color-background-active:var(--ifm-hover-overlay);--ifm-pills-spacing:0.125rem;--ifm-tabs-color:var(--ifm-font-color-secondary);--ifm-tabs-color-active:var(--ifm-color-primary);--ifm-tabs-color-active-border:var(--ifm-tabs-color-active);--ifm-tabs-padding-horizontal:1rem;--ifm-tabs-padding-vertical:1rem;--docusaurus-progress-bar-color:var(--ifm-color-primary);--ifm-font-family-base:"IamwriterquattroS";--docusaurus-announcement-bar-height:auto;--docusaurus-collapse-button-bg:#0000;--docusaurus-collapse-button-bg-hover:#0000001a;--doc-sidebar-width:300px;--doc-sidebar-hidden-width:30px;--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:#656c85cc;--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 #ffffff80,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 #1e235a66;--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 #45629b1f;--docsearch-primary-color:var(--ifm-color-primary);--docsearch-text-color:var(--ifm-font-color-base);--docusaurus-tag-list-border:var(--ifm-color-emphasis-300)}.badge--danger,.badge--info,.badge--primary,.badge--secondary,.badge--success,.badge--warning{--ifm-badge-border-color:var(--ifm-badge-background-color)}.button--link,.button--outline{--ifm-button-background-color:#0000}html{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--ifm-background-color);color:var(--ifm-font-color-base);color-scheme:var(--ifm-color-scheme);font:var(--ifm-font-size-base)/var(--ifm-line-height-base) var(--ifm-font-family-base);text-rendering:optimizelegibility}iframe{border:0;color-scheme:auto}.container{margin:0 auto;max-width:var(--ifm-container-width)}.container--fluid{max-width:inherit}.row{display:flex;flex-wrap:wrap;margin:0 calc(var(--ifm-spacing-horizontal)*-1)}.margin-bottom--none,.margin-vert--none,.markdown>:last-child{margin-bottom:0!important}.margin-top--none,.margin-vert--none{margin-top:0!important}.row--no-gutters{margin-left:0;margin-right:0}.margin-horiz--none,.margin-right--none{margin-right:0!important}.row--no-gutters>.col{padding-left:0;padding-right:0}.row--align-top{align-items:flex-start}.row--align-bottom{align-items:flex-end}.row--align-center{align-items:center}.row--align-stretch{align-items:stretch}.row--align-baseline{align-items:baseline}.col{--ifm-col-width:100%;flex:1 0;margin-left:0;max-width:var(--ifm-col-width)}.padding-bottom--none,.padding-vert--none{padding-bottom:0!important}.padding-top--none,.padding-vert--none{padding-top:0!important}.padding-horiz--none,.padding-left--none{padding-left:0!important}.padding-horiz--none,.padding-right--none{padding-right:0!important}.col[class*=col--]{flex:0 0 var(--ifm-col-width)}.col--1{--ifm-col-width:8.33333%}.col--offset-1{margin-left:8.33333%}.col--2{--ifm-col-width:16.66667%}.col--offset-2{margin-left:16.66667%}.col--3{--ifm-col-width:25%}.col--offset-3{margin-left:25%}.col--4{--ifm-col-width:33.33333%}.col--offset-4{margin-left:33.33333%}.col--5{--ifm-col-width:41.66667%}.col--offset-5{margin-left:41.66667%}.col--6{--ifm-col-width:50%}.col--offset-6{margin-left:50%}.col--7{--ifm-col-width:58.33333%}.col--offset-7{margin-left:58.33333%}.col--8{--ifm-col-width:66.66667%}.col--offset-8{margin-left:66.66667%}.col--9{--ifm-col-width:75%}.col--offset-9{margin-left:75%}.col--10{--ifm-col-width:83.33333%}.col--offset-10{margin-left:83.33333%}.col--11{--ifm-col-width:91.66667%}.col--offset-11{margin-left:91.66667%}.col--12{--ifm-col-width:100%}.col--offset-12{margin-left:100%}.margin-horiz--none,.margin-left--none{margin-left:0!important}.margin--none{margin:0!important}.margin-bottom--xs,.margin-vert--xs{margin-bottom:.25rem!important}.margin-top--xs,.margin-vert--xs{margin-top:.25rem!important}.margin-horiz--xs,.margin-left--xs{margin-left:.25rem!important}.margin-horiz--xs,.margin-right--xs{margin-right:.25rem!important}.margin--xs{margin:.25rem!important}.margin-bottom--sm,.margin-vert--sm{margin-bottom:.5rem!important}.margin-top--sm,.margin-vert--sm{margin-top:.5rem!important}.margin-horiz--sm,.margin-left--sm{margin-left:.5rem!important}.margin-horiz--sm,.margin-right--sm{margin-right:.5rem!important}.margin--sm{margin:.5rem!important}.margin-bottom--md,.margin-vert--md{margin-bottom:1rem!important}.margin-top--md,.margin-vert--md{margin-top:1rem!important}.margin-horiz--md,.margin-left--md{margin-left:1rem!important}.margin-horiz--md,.margin-right--md{margin-right:1rem!important}.margin--md{margin:1rem!important}.margin-bottom--lg,.margin-vert--lg{margin-bottom:2rem!important}.margin-top--lg,.margin-vert--lg{margin-top:2rem!important}.margin-horiz--lg,.margin-left--lg{margin-left:2rem!important}.margin-horiz--lg,.margin-right--lg{margin-right:2rem!important}.margin--lg{margin:2rem!important}.margin-bottom--xl,.margin-vert--xl{margin-bottom:5rem!important}.margin-top--xl,.margin-vert--xl{margin-top:5rem!important}.margin-horiz--xl,.margin-left--xl{margin-left:5rem!important}.margin-horiz--xl,.margin-right--xl{margin-right:5rem!important}.margin--xl{margin:5rem!important}.padding--none{padding:0!important}.padding-bottom--xs,.padding-vert--xs{padding-bottom:.25rem!important}.padding-top--xs,.padding-vert--xs{padding-top:.25rem!important}.padding-horiz--xs,.padding-left--xs{padding-left:.25rem!important}.padding-horiz--xs,.padding-right--xs{padding-right:.25rem!important}.padding--xs{padding:.25rem!important}.padding-bottom--sm,.padding-vert--sm{padding-bottom:.5rem!important}.padding-top--sm,.padding-vert--sm{padding-top:.5rem!important}.padding-horiz--sm,.padding-left--sm{padding-left:.5rem!important}.padding-horiz--sm,.padding-right--sm{padding-right:.5rem!important}.padding--sm{padding:.5rem!important}.padding-bottom--md,.padding-vert--md{padding-bottom:1rem!important}.padding-top--md,.padding-vert--md{padding-top:1rem!important}.padding-horiz--md,.padding-left--md{padding-left:1rem!important}.padding-horiz--md,.padding-right--md{padding-right:1rem!important}.padding--md{padding:1rem!important}.padding-bottom--lg,.padding-vert--lg{padding-bottom:2rem!important}.padding-top--lg,.padding-vert--lg{padding-top:2rem!important}.padding-horiz--lg,.padding-left--lg{padding-left:2rem!important}.padding-horiz--lg,.padding-right--lg{padding-right:2rem!important}.padding--lg{padding:2rem!important}.padding-bottom--xl,.padding-vert--xl{padding-bottom:5rem!important}.padding-top--xl,.padding-vert--xl{padding-top:5rem!important}.padding-horiz--xl,.padding-left--xl{padding-left:5rem!important}.padding-horiz--xl,.padding-right--xl{padding-right:5rem!important}.padding--xl{padding:5rem!important}code{background-color:var(--ifm-code-background);border:.1rem solid #0000001a;border-radius:var(--ifm-code-border-radius);font-family:var(--ifm-font-family-monospace);font-size:var(--ifm-code-font-size);padding:var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal)}a code{color:inherit}pre{background-color:var(--ifm-pre-background);border-radius:var(--ifm-pre-border-radius);color:var(--ifm-pre-color);font:var(--ifm-code-font-size)/var(--ifm-pre-line-height) var(--ifm-font-family-monospace);padding:var(--ifm-pre-padding)}pre code{background-color:initial;border:none;font-size:100%;line-height:inherit;padding:0}kbd{background-color:var(--ifm-color-emphasis-0);border:1px solid var(--ifm-color-emphasis-400);border-radius:.2rem;box-shadow:inset 0 -1px 0 var(--ifm-color-emphasis-400);color:var(--ifm-color-emphasis-800);font:80% var(--ifm-font-family-monospace);padding:.15rem .3rem}h1,h2,h3,h4,h5,h6{color:var(--ifm-heading-color);font-family:var(--ifm-heading-font-family);font-weight:var(--ifm-heading-font-weight);line-height:var(--ifm-heading-line-height);margin:var(--ifm-heading-margin-top) 0 var(--ifm-heading-margin-bottom) 0}h1{font-size:var(--ifm-h1-font-size)}h2{font-size:var(--ifm-h2-font-size)}h3{font-size:var(--ifm-h3-font-size)}h4{font-size:var(--ifm-h4-font-size)}h5{font-size:var(--ifm-h5-font-size)}h6{font-size:var(--ifm-h6-font-size)}.max-width-100,img{max-width:100%}img[align=right]{padding-left:var(--image-alignment-padding)}img[align=left]{padding-right:var(--image-alignment-padding)}.markdown{--ifm-h1-vertical-rhythm-top:3;--ifm-h2-vertical-rhythm-top:2;--ifm-h3-vertical-rhythm-top:1.5;--ifm-heading-vertical-rhythm-top:1.25;--ifm-h1-vertical-rhythm-bottom:1.25;--ifm-heading-vertical-rhythm-bottom:1}.markdown:after,.markdown:before{content:"";display:table}.markdown:after{clear:both}.markdown h1:first-child{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-h1-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown>h2{--ifm-h2-font-size:2rem;margin-top:calc(var(--ifm-h2-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h3{--ifm-h3-font-size:1.5rem;margin-top:calc(var(--ifm-h3-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h4,.markdown>h5,.markdown>h6{margin-top:calc(var(--ifm-heading-vertical-rhythm-top)*var(--ifm-leading))}.markdown>p,.markdown>pre,.markdown>ul{margin-bottom:var(--ifm-leading)}.markdown li>p{margin-top:var(--ifm-list-paragraph-margin)}.markdown li+li{margin-top:var(--ifm-list-item-margin)}ol,ul{margin:0 0 var(--ifm-list-margin);padding-left:var(--ifm-list-left-padding)}ol ol,ul ol{list-style-type:lower-roman}ol ol ol,ol ul ol,ul ol ol,ul ul ol{list-style-type:lower-alpha}table{border-collapse:collapse;display:block;margin-bottom:var(--ifm-spacing-vertical)}table thead tr{border-bottom:2px solid var(--ifm-table-border-color)}table thead,table tr:nth-child(2n){background-color:var(--ifm-table-stripe-background)}table tr{background-color:var(--ifm-table-background);border-top:var(--ifm-table-border-width) solid var(--ifm-table-border-color)}table td,table th{border:var(--ifm-table-border-width) solid var(--ifm-table-border-color);padding:var(--ifm-table-cell-padding)}table th{background-color:var(--ifm-table-head-background);color:var(--ifm-table-head-color);font-weight:var(--ifm-table-head-font-weight)}table td{color:var(--ifm-table-cell-color)}strong{font-weight:var(--ifm-font-weight-bold)}a{color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}a:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button:hover,.text--no-decoration,.text--no-decoration:hover,a:not([href]){text-decoration:none}p{margin:0 0 var(--ifm-paragraph-margin-bottom)}blockquote{border-left:var(--ifm-blockquote-border-left-width) solid var(--ifm-blockquote-border-color);box-shadow:var(--ifm-blockquote-shadow);color:var(--ifm-blockquote-color);font-size:var(--ifm-blockquote-font-size);padding:var(--ifm-blockquote-padding-vertical) var(--ifm-blockquote-padding-horizontal)}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}hr{background-color:var(--ifm-hr-background-color);border:0;height:var(--ifm-hr-height);margin:var(--ifm-hr-margin-vertical) 0}.shadow--lw{box-shadow:var(--ifm-global-shadow-lw)!important}.shadow--md{box-shadow:var(--ifm-global-shadow-md)!important}.shadow--tl{box-shadow:var(--ifm-global-shadow-tl)!important}.text--primary{color:var(--ifm-color-primary)}.text--secondary{color:var(--ifm-color-secondary)}.text--success{color:var(--ifm-color-success)}.text--info{color:var(--ifm-color-info)}.text--warning{color:var(--ifm-color-warning)}.text--danger{color:var(--ifm-color-danger)}.text--center{text-align:center}.text--left{text-align:left}.text--justify{text-align:justify}.text--right{text-align:right}.text--capitalize{text-transform:capitalize}.text--lowercase{text-transform:lowercase}.admonitionHeading_Gvgb,.alert__heading,.footer-ls-section-title,.hp-section-header,.text--uppercase{text-transform:uppercase}.text--light{font-weight:var(--ifm-font-weight-light)}.text--normal{font-weight:var(--ifm-font-weight-normal)}.text--semibold{font-weight:var(--ifm-font-weight-semibold)}.text--bold{font-weight:var(--ifm-font-weight-bold)}.text--italic{font-style:italic}.text--truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text--break{word-wrap:break-word!important;word-break:break-word!important}.clean-btn{background:none;border:none;color:inherit;cursor:pointer;font-family:inherit;padding:0}.alert,.alert .close{color:var(--ifm-alert-foreground-color)}.clean-list{list-style:none;padding-left:0}.alert--primary{--ifm-alert-background-color:var(--ifm-color-primary-contrast-background);--ifm-alert-background-color-highlight:#3578e526;--ifm-alert-foreground-color:var(--ifm-color-primary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-primary-dark)}.alert--secondary{--ifm-alert-background-color:var(--ifm-color-secondary-contrast-background);--ifm-alert-background-color-highlight:#ebedf026;--ifm-alert-foreground-color:var(--ifm-color-secondary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-secondary-dark)}.alert--success{--ifm-alert-background-color:var(--ifm-color-success-contrast-background);--ifm-alert-background-color-highlight:#00a40026;--ifm-alert-foreground-color:var(--ifm-color-success-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-success-dark)}.alert--info{--ifm-alert-background-color:var(--ifm-color-info-contrast-background);--ifm-alert-background-color-highlight:#54c7ec26;--ifm-alert-foreground-color:var(--ifm-color-info-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-info-dark)}.alert--warning{--ifm-alert-background-color:var(--ifm-color-warning-contrast-background);--ifm-alert-background-color-highlight:#ffba0026;--ifm-alert-foreground-color:var(--ifm-color-warning-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-warning-dark)}.alert--danger{--ifm-alert-background-color:var(--ifm-color-danger-contrast-background);--ifm-alert-background-color-highlight:#fa383e26;--ifm-alert-foreground-color:var(--ifm-color-danger-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-danger-dark)}.alert{--ifm-code-background:var(--ifm-alert-background-color-highlight);--ifm-link-color:var(--ifm-alert-foreground-color);--ifm-link-hover-color:var(--ifm-alert-foreground-color);--ifm-link-decoration:underline;--ifm-tabs-color:var(--ifm-alert-foreground-color);--ifm-tabs-color-active:var(--ifm-alert-foreground-color);--ifm-tabs-color-active-border:var(--ifm-alert-border-color);background-color:var(--ifm-alert-background-color);border:var(--ifm-alert-border-width) solid var(--ifm-alert-border-color);border-left-width:var(--ifm-alert-border-left-width);border-radius:var(--ifm-alert-border-radius);box-shadow:var(--ifm-alert-shadow);padding:var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal)}.alert__heading{align-items:center;display:flex;font:700 var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family);margin-bottom:.5rem}.alert__icon{display:inline-flex;margin-right:.4em}.alert__icon svg{fill:var(--ifm-alert-foreground-color);stroke:var(--ifm-alert-foreground-color);stroke-width:0}.alert .close{margin:calc(var(--ifm-alert-padding-vertical)*-1) calc(var(--ifm-alert-padding-horizontal)*-1) 0 0;opacity:.75}.alert .close:focus,.alert .close:hover{opacity:1}.alert a{text-decoration-color:var(--ifm-alert-border-color)}.alert a:hover{text-decoration-thickness:2px}.avatar{column-gap:var(--ifm-avatar-intro-margin);display:flex}.avatar__photo{border-radius:50%;display:block;height:var(--ifm-avatar-photo-size);overflow:hidden;width:var(--ifm-avatar-photo-size)}.card--full-height,.navbar__logo img,body,html{height:100%}.avatar__photo--sm{--ifm-avatar-photo-size:2rem}.avatar__photo--lg{--ifm-avatar-photo-size:4rem}.avatar__photo--xl{--ifm-avatar-photo-size:6rem}.avatar__intro{display:flex;flex:1 1;flex-direction:column;justify-content:center;text-align:var(--ifm-avatar-intro-alignment)}.badge,.breadcrumbs__item,.breadcrumbs__link,.button,.dropdown>.navbar__link:after{display:inline-block}.avatar__name{font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base)}.avatar__subtitle{margin-top:.25rem}.avatar--vertical{--ifm-avatar-intro-alignment:center;--ifm-avatar-intro-margin:0.5rem;align-items:center;flex-direction:column}.badge{background-color:var(--ifm-badge-background-color);border:var(--ifm-badge-border-width) solid var(--ifm-badge-border-color);border-radius:var(--ifm-badge-border-radius);color:var(--ifm-badge-color);font-size:75%;font-weight:var(--ifm-font-weight-bold);line-height:1;padding:var(--ifm-badge-padding-vertical) var(--ifm-badge-padding-horizontal)}.badge--primary{--ifm-badge-background-color:var(--ifm-color-primary)}.badge--secondary{--ifm-badge-background-color:var(--ifm-color-secondary);color:var(--ifm-color-black)}.breadcrumbs__link,.button.button--secondary.button--outline:not(.button--active):not(:hover){color:var(--ifm-font-color-base)}.badge--success{--ifm-badge-background-color:var(--ifm-color-success)}.badge--info{--ifm-badge-background-color:var(--ifm-color-info)}.badge--warning{--ifm-badge-background-color:var(--ifm-color-warning)}.badge--danger{--ifm-badge-background-color:var(--ifm-color-danger)}.breadcrumbs{margin-bottom:0;padding-left:0}.breadcrumbs__item:not(:last-child):after{background:var(--ifm-breadcrumb-separator) center;content:" ";display:inline-block;filter:var(--ifm-breadcrumb-separator-filter);height:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier));margin:0 var(--ifm-breadcrumb-spacing);opacity:.5;width:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier))}.breadcrumbs__item--active .breadcrumbs__link{background:var(--ifm-breadcrumb-item-background-active);color:var(--ifm-breadcrumb-color-active)}.breadcrumbs__link{border-radius:var(--ifm-breadcrumb-border-radius);font-size:calc(1rem*var(--ifm-breadcrumb-size-multiplier));padding:calc(var(--ifm-breadcrumb-padding-vertical)*var(--ifm-breadcrumb-size-multiplier)) calc(var(--ifm-breadcrumb-padding-horizontal)*var(--ifm-breadcrumb-size-multiplier));transition-duration:var(--ifm-transition-fast);transition-property:background,color}.breadcrumbs__link:any-link:hover,.breadcrumbs__link:link:hover,.breadcrumbs__link:visited:hover,area[href].breadcrumbs__link:hover{background:var(--ifm-breadcrumb-item-background-active);text-decoration:none}.breadcrumbs--sm{--ifm-breadcrumb-size-multiplier:0.8}.breadcrumbs--lg{--ifm-breadcrumb-size-multiplier:1.2}.button{background-color:var(--ifm-button-background-color);border:var(--ifm-button-border-width) solid var(--ifm-button-border-color);border-radius:var(--ifm-button-border-radius);cursor:pointer;font-size:calc(.875rem*var(--ifm-button-size-multiplier));font-weight:var(--ifm-button-font-weight);line-height:1.5;padding:calc(var(--ifm-button-padding-vertical)*var(--ifm-button-size-multiplier)) calc(var(--ifm-button-padding-horizontal)*var(--ifm-button-size-multiplier));text-align:center;transition-duration:var(--ifm-button-transition-duration);transition-property:color,background,border-color;-webkit-user-select:none;user-select:none;white-space:nowrap}.button,.button:hover{color:var(--ifm-button-color)}.button--outline{--ifm-button-color:var(--ifm-button-border-color)}.button--outline:hover{--ifm-button-background-color:var(--ifm-button-border-color)}.button--link{--ifm-button-border-color:#0000;color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}.button--link.button--active,.button--link:active,.button--link:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button.disabled,.button:disabled,.button[disabled]{opacity:.65;pointer-events:none}.button--sm{--ifm-button-size-multiplier:0.8}.button--lg{--ifm-button-size-multiplier:1.35}.button--block{display:block;width:100%}.button.button--secondary{color:var(--ifm-color-gray-900)}:where(.button--primary){--ifm-button-background-color:var(--ifm-color-primary);--ifm-button-border-color:var(--ifm-color-primary)}:where(.button--primary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-primary-dark);--ifm-button-border-color:var(--ifm-color-primary-dark)}.button--primary.button--active,.button--primary:active{--ifm-button-background-color:var(--ifm-color-primary-darker);--ifm-button-border-color:var(--ifm-color-primary-darker)}:where(.button--secondary){--ifm-button-background-color:var(--ifm-color-secondary);--ifm-button-border-color:var(--ifm-color-secondary)}:where(.button--secondary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-secondary-dark);--ifm-button-border-color:var(--ifm-color-secondary-dark)}.button--secondary.button--active,.button--secondary:active{--ifm-button-background-color:var(--ifm-color-secondary-darker);--ifm-button-border-color:var(--ifm-color-secondary-darker)}:where(.button--success){--ifm-button-background-color:var(--ifm-color-success);--ifm-button-border-color:var(--ifm-color-success)}:where(.button--success):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-success-dark);--ifm-button-border-color:var(--ifm-color-success-dark)}.button--success.button--active,.button--success:active{--ifm-button-background-color:var(--ifm-color-success-darker);--ifm-button-border-color:var(--ifm-color-success-darker)}:where(.button--info){--ifm-button-background-color:var(--ifm-color-info);--ifm-button-border-color:var(--ifm-color-info)}:where(.button--info):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-info-dark);--ifm-button-border-color:var(--ifm-color-info-dark)}.button--info.button--active,.button--info:active{--ifm-button-background-color:var(--ifm-color-info-darker);--ifm-button-border-color:var(--ifm-color-info-darker)}:where(.button--warning){--ifm-button-background-color:var(--ifm-color-warning);--ifm-button-border-color:var(--ifm-color-warning)}:where(.button--warning):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-warning-dark);--ifm-button-border-color:var(--ifm-color-warning-dark)}.button--warning.button--active,.button--warning:active{--ifm-button-background-color:var(--ifm-color-warning-darker);--ifm-button-border-color:var(--ifm-color-warning-darker)}:where(.button--danger){--ifm-button-background-color:var(--ifm-color-danger);--ifm-button-border-color:var(--ifm-color-danger)}:where(.button--danger):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-danger-dark);--ifm-button-border-color:var(--ifm-color-danger-dark)}.button--danger.button--active,.button--danger:active{--ifm-button-background-color:var(--ifm-color-danger-darker);--ifm-button-border-color:var(--ifm-color-danger-darker)}.button-group{display:inline-flex;gap:var(--ifm-button-group-spacing)}.button-group>.button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.button-group>.button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.button-group--block{display:flex;justify-content:stretch}.button-group--block>.button{flex-grow:1}.card{background-color:var(--ifm-card-background-color);border-radius:var(--ifm-card-border-radius);box-shadow:var(--ifm-global-shadow-lw);display:flex;flex-direction:column;overflow:hidden}.card__image{padding-top:var(--ifm-card-vertical-spacing)}.card__image:first-child{padding-top:0}.card__body,.card__footer,.card__header{padding:var(--ifm-card-vertical-spacing) var(--ifm-card-horizontal-spacing)}.card__body:not(:last-child),.card__footer:not(:last-child),.card__header:not(:last-child){padding-bottom:0}.card__body>:last-child,.card__footer>:last-child,.card__header>:last-child{margin-bottom:0}.card__footer{margin-top:auto}.table-of-contents{font-size:.8rem;margin-bottom:0;padding:var(--ifm-toc-padding-vertical) 0}.table-of-contents,.table-of-contents ul{list-style:none;padding-left:var(--ifm-toc-padding-horizontal)}.table-of-contents li{margin:var(--ifm-toc-padding-vertical) var(--ifm-toc-padding-horizontal)}.table-of-contents__left-border{border-left:1px solid var(--ifm-toc-border-color)}.table-of-contents__link{color:var(--ifm-toc-link-color);display:block}.table-of-contents__link--active,.table-of-contents__link--active code,.table-of-contents__link:hover,.table-of-contents__link:hover code{color:var(--ifm-color-primary);text-decoration:none}.close{color:var(--ifm-color-black);float:right;font-size:1.5rem;font-weight:var(--ifm-font-weight-bold);line-height:1;opacity:.5;padding:1rem;transition:opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.close:hover{opacity:.7}.close:focus{opacity:.8}.dropdown{display:inline-flex;font-weight:var(--ifm-dropdown-font-weight);position:relative;vertical-align:top}.dropdown--hoverable:hover .dropdown__menu,.dropdown--show .dropdown__menu{opacity:1;pointer-events:all;transform:translateY(-1px);visibility:visible}#nprogress,.dropdown__menu,.navbar__item.dropdown .navbar__link:not([href]){pointer-events:none}.dropdown--right .dropdown__menu{left:inherit;right:0}.dropdown--nocaret .navbar__link:after{content:none!important}.dropdown__menu{background-color:var(--ifm-dropdown-background-color);border-radius:var(--ifm-global-radius);box-shadow:var(--ifm-global-shadow-md);left:0;list-style:none;max-height:80vh;min-width:10rem;opacity:0;overflow-y:auto;padding:.5rem;position:absolute;top:calc(100% - var(--ifm-navbar-item-padding-vertical) + .3rem);transform:translateY(-.625rem);transition-duration:var(--ifm-transition-fast);transition-property:opacity,transform,visibility;transition-timing-function:var(--ifm-transition-timing-default);visibility:hidden;z-index:var(--ifm-z-index-dropdown)}.menu__caret,.menu__link,.menu__list-item-collapsible{border-radius:.25rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.dropdown__link{border-radius:.25rem;color:var(--ifm-dropdown-link-color);display:block;font-size:.875rem;margin-top:.2rem;padding:.25rem .5rem;white-space:nowrap}.dropdown__link--active,.dropdown__link:hover{background-color:var(--ifm-dropdown-hover-background-color);color:var(--ifm-dropdown-link-color);text-decoration:none}.dropdown__link--active,.dropdown__link--active:hover{--ifm-dropdown-link-color:var(--ifm-link-color)}.dropdown>.navbar__link:after{border-color:currentcolor #0000;border-style:solid;border-width:.4em .4em 0;content:"";margin-left:.3em;position:relative;top:2px;transform:translateY(-50%)}.details_lb9f[data-collapsed=false].isBrowser_bmU9>summary:before,.details_lb9f[open]:not(.isBrowser_bmU9)>summary:before,.menu__list-item--collapsed .menu__caret:before,.menu__list-item--collapsed .menu__link--sublist:after{transform:rotate(90deg)}.footer{background-color:var(--ifm-footer-background-color);color:var(--ifm-footer-color);padding:var(--ifm-footer-padding-vertical) var(--ifm-footer-padding-horizontal)}.footer--dark{--ifm-footer-background-color:#303846;--ifm-footer-color:var(--ifm-footer-link-color);--ifm-footer-link-color:var(--ifm-color-secondary);--ifm-footer-title-color:var(--ifm-color-white)}.footer__links{margin-bottom:1rem}.footer__link-item{color:var(--ifm-footer-link-color);line-height:2}.footer__link-item:hover{color:var(--ifm-footer-link-hover-color)}.footer__link-separator{margin:0 var(--ifm-footer-link-horizontal-spacing)}.footer__logo{margin-top:1rem;max-width:var(--ifm-footer-logo-max-width)}.docItemContainer_Djhp article>:first-child,.docItemContainer_Djhp header+*,.footer__item{margin-top:0}.footer__title{color:var(--ifm-footer-title-color);font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base);margin-bottom:var(--ifm-heading-margin-bottom)}.menu,.navbar__link{font-weight:var(--ifm-font-weight-semibold)}.footer__items{margin-bottom:0}[type=checkbox]{padding:0}.hero{align-items:center;background-color:var(--ifm-hero-background-color);color:var(--ifm-hero-text-color);display:flex;padding:4rem 2rem}.hero--primary{--ifm-hero-background-color:var(--ifm-color-primary);--ifm-hero-text-color:var(--ifm-font-color-base-inverse)}.hero--dark{--ifm-hero-background-color:#303846;--ifm-hero-text-color:var(--ifm-color-white)}.hero__title{font-size:3rem}.hero__subtitle{font-size:1.5rem}.menu__list{list-style:none;margin:0;padding-left:0}.menu__caret,.menu__link{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu__list .menu__list{flex:0 0 100%;margin-top:.25rem;padding-left:var(--ifm-menu-link-padding-horizontal)}.menu__list-item:not(:first-child){margin-top:.25rem}.menu__list-item--collapsed .menu__list{height:0;overflow:hidden}.menu__list-item-collapsible{display:flex;flex-wrap:wrap;position:relative}.menu__caret:hover,.menu__link:hover,.menu__list-item-collapsible--active,.menu__list-item-collapsible:hover{background:var(--ifm-menu-color-background-hover)}.menu__list-item-collapsible .menu__link--active,.menu__list-item-collapsible .menu__link:hover{background:none!important}.menu__caret,.menu__link{align-items:center;display:flex}.navbar-sidebar,.navbar-sidebar__backdrop{bottom:0;opacity:0;transition-duration:var(--ifm-transition-fast);transition-timing-function:ease-in-out;left:0;top:0;visibility:hidden}.menu__link{color:var(--ifm-menu-color);flex:1;line-height:1.25}.menu__link:hover{color:var(--ifm-menu-color);text-decoration:none}.menu__caret:before,.menu__link--sublist-caret:after{content:"";height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast) linear;width:1.25rem;filter:var(--ifm-menu-link-sublist-icon-filter)}.menu__link--sublist-caret:after{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem;margin-left:auto;min-width:1.25rem}.menu__link--active,.menu__link--active:hover{color:var(--ifm-menu-color-active)}.navbar__brand,.navbar__link{color:var(--ifm-navbar-link-color)}.menu__link--active:not(.menu__link--sublist){background-color:var(--ifm-menu-color-background-active)}.menu__caret:before{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem}.navbar--dark,html[data-theme=dark]{--ifm-menu-link-sublist-icon-filter:invert(100%) sepia(94%) saturate(17%) hue-rotate(223deg) brightness(104%) contrast(98%)}.navbar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-navbar-shadow);height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar,.navbar>.container,.navbar>.container-fluid{display:flex}.navbar--fixed-top{position:sticky;top:0;z-index:var(--ifm-z-index-fixed)}.navbar__inner{display:flex;flex-wrap:wrap;justify-content:space-between;width:100%}.navbar__brand{align-items:center;display:flex;margin-right:1rem;min-width:0}.navbar__brand:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.navbar__title{flex:1 1 auto}.navbar__toggle{display:none;margin-right:.5rem}.navbar__logo{flex:0 0 auto;height:2rem;margin-right:.5rem}.navbar__items{align-items:center;display:flex;flex:1;min-width:0}.navbar__items--center{flex:0 0 auto}.navbar__items--center .navbar__brand{margin:0}.navbar__items--center+.navbar__items--right{flex:1}.navbar__items--right{flex:0 0 auto;justify-content:flex-end}.navbar__items--right>:last-child{padding-right:0}.navbar__item{display:inline-block;padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.navbar__link--active,.navbar__link:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.navbar--dark,.navbar--primary{--ifm-menu-color:var(--ifm-color-gray-300);--ifm-navbar-link-color:var(--ifm-color-gray-100);--ifm-navbar-search-input-background-color:#ffffff1a;--ifm-navbar-search-input-placeholder-color:#ffffff80;color:var(--ifm-color-white)}.navbar--dark{--ifm-navbar-background-color:#242526;--ifm-menu-color-background-active:#ffffff0d;--ifm-navbar-search-input-color:var(--ifm-color-white)}.navbar--primary{--ifm-navbar-background-color:var(--ifm-color-primary);--ifm-navbar-link-hover-color:var(--ifm-color-white);--ifm-menu-color-active:var(--ifm-color-white);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-500)}.navbar__search-input{-webkit-appearance:none;appearance:none;background:var(--ifm-navbar-search-input-background-color) var(--ifm-navbar-search-input-icon) no-repeat .75rem center/1rem 1rem;border:none;border-radius:2rem;color:var(--ifm-navbar-search-input-color);cursor:text;display:inline-block;font-size:.9rem;height:2rem;padding:0 .5rem 0 2.25rem;width:12.5rem}.navbar__search-input::placeholder{color:var(--ifm-navbar-search-input-placeholder-color)}.navbar-sidebar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-global-shadow-md);position:fixed;transform:translate3d(-100%,0,0);transition-property:opacity,visibility,transform;width:var(--ifm-navbar-sidebar-width)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar__items{transform:translateZ(0)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar--show .navbar-sidebar__backdrop{opacity:1;visibility:visible}.navbar-sidebar__backdrop{background-color:#0009;position:fixed;right:0;transition-property:opacity,visibility}.navbar-sidebar__brand{align-items:center;box-shadow:var(--ifm-navbar-shadow);display:flex;flex:1;height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar-sidebar__items{display:flex;height:calc(100% - var(--ifm-navbar-height));transition:transform var(--ifm-transition-fast) ease-in-out}.navbar-sidebar__items--show-secondary{transform:translate3d(calc((var(--ifm-navbar-sidebar-width))*-1),0,0)}.navbar-sidebar__item{flex-shrink:0;padding:.5rem;width:calc(var(--ifm-navbar-sidebar-width))}.navbar-sidebar__back{background:var(--ifm-menu-color-background-active);font-size:15px;font-weight:var(--ifm-button-font-weight);margin:0 0 .2rem -.5rem;padding:.6rem 1.5rem;position:relative;text-align:left;top:-.5rem;width:calc(100% + 1rem)}.navbar-sidebar__close{display:flex;margin-left:auto}.pagination{column-gap:var(--ifm-pagination-page-spacing);display:flex;font-size:var(--ifm-pagination-font-size);padding-left:0}.pagination--sm{--ifm-pagination-font-size:0.8rem;--ifm-pagination-padding-horizontal:0.8rem;--ifm-pagination-padding-vertical:0.2rem}.pagination--lg{--ifm-pagination-font-size:1.2rem;--ifm-pagination-padding-horizontal:1.2rem;--ifm-pagination-padding-vertical:0.3rem}.pagination__item{display:inline-flex}.pagination__item>span{padding:var(--ifm-pagination-padding-vertical)}.pagination__item--active .pagination__link{color:var(--ifm-pagination-color-active)}.pagination__item--active .pagination__link,.pagination__item:not(.pagination__item--active):hover .pagination__link{background:var(--ifm-pagination-item-active-background)}.pagination__item--disabled,.pagination__item[disabled]{opacity:.25;pointer-events:none}.pagination__link{border-radius:var(--ifm-pagination-border-radius);color:var(--ifm-font-color-base);display:inline-block;padding:var(--ifm-pagination-padding-vertical) var(--ifm-pagination-padding-horizontal);transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination__link:hover{text-decoration:none}.pagination-nav{grid-gap:var(--ifm-spacing-horizontal);display:grid;gap:var(--ifm-spacing-horizontal);grid-template-columns:repeat(2,1fr)}.pagination-nav__link{border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-pagination-nav-border-radius);display:block;height:100%;line-height:var(--ifm-heading-line-height);padding:var(--ifm-global-spacing);transition:border-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination-nav__link:hover{border-color:var(--ifm-pagination-nav-color-hover);text-decoration:none}.pagination-nav__link--next{grid-column:2/3;text-align:right}.pagination-nav__label{font-size:var(--ifm-h4-font-size);font-weight:var(--ifm-heading-font-weight);word-break:break-word}.pagination-nav__link--prev .pagination-nav__label:before{content:"« "}.pagination-nav__link--next .pagination-nav__label:after{content:" »"}.pagination-nav__sublabel{color:var(--ifm-color-content-secondary);font-size:var(--ifm-h5-font-size);font-weight:var(--ifm-font-weight-semibold);margin-bottom:.25rem}.pills__item,.tabs{font-weight:var(--ifm-font-weight-bold)}.admonitionContent_BuS1>:last-child,.bc-chat-popup p,.cardContainer_fWXF :last-child,.collapsibleContent_i85q p:last-child,.details_lb9f>summary>p:last-child,.tabItem_Ymn6>:last-child,.tabs,.terminalWindowBody_tzdS :last-child{margin-bottom:0}.pills{display:flex;gap:var(--ifm-pills-spacing);padding-left:0}.pills__item{border-radius:.5rem;cursor:pointer;display:inline-block;padding:.25rem 1rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs,:not(.containsTaskList_mC6p>li)>.containsTaskList_mC6p{padding-left:0}.pills__item--active{color:var(--ifm-pills-color-active)}.pills__item--active,.pills__item:not(.pills__item--active):hover{background:var(--ifm-pills-color-background-active)}.app-cta,.bc-layout,.bc-thumbs-button{background-color:#fff}.pills--block{justify-content:stretch}.pills--block .pills__item{flex-grow:1;text-align:center}.tabs{color:var(--ifm-tabs-color);display:flex;overflow-x:auto}.bc-input,.footer-cr a{font-weight:700}.tabs__item{border-bottom:3px solid #0000;border-radius:var(--ifm-global-radius);cursor:pointer;display:inline-flex;padding:var(--ifm-tabs-padding-vertical) var(--ifm-tabs-padding-horizontal);transition:background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs__item--active{border-bottom-color:var(--ifm-tabs-color-active-border);border-bottom-left-radius:0;border-bottom-right-radius:0;color:var(--ifm-tabs-color-active)}.tabs__item:hover{background-color:var(--ifm-hover-overlay)}.tabs--block{justify-content:stretch}.tabs--block .tabs__item{flex-grow:1;justify-content:center}.hp-list,.hp-section{flex-direction:column;display:flex}html[data-theme=dark]{--ifm-color-scheme:dark;--ifm-color-emphasis-0:var(--ifm-color-gray-1000);--ifm-color-emphasis-100:var(--ifm-color-gray-900);--ifm-color-emphasis-200:var(--ifm-color-gray-800);--ifm-color-emphasis-300:var(--ifm-color-gray-700);--ifm-color-emphasis-400:var(--ifm-color-gray-600);--ifm-color-emphasis-600:var(--ifm-color-gray-400);--ifm-color-emphasis-700:var(--ifm-color-gray-300);--ifm-color-emphasis-800:var(--ifm-color-gray-200);--ifm-color-emphasis-900:var(--ifm-color-gray-100);--ifm-color-emphasis-1000:var(--ifm-color-gray-0);--ifm-background-color:#1b1b1d;--ifm-background-surface-color:#242526;--ifm-hover-overlay:#ffffff0d;--ifm-color-content:#e3e3e3;--ifm-color-content-secondary:#fff;--ifm-breadcrumb-separator-filter:invert(64%) sepia(11%) saturate(0%) hue-rotate(149deg) brightness(99%) contrast(95%);--ifm-code-background:#ffffff1a;--ifm-scrollbar-track-background-color:#444;--ifm-scrollbar-thumb-background-color:#686868;--ifm-scrollbar-thumb-hover-background-color:#7a7a7a;--ifm-table-stripe-background:#ffffff12;--ifm-toc-border-color:var(--ifm-color-emphasis-200);--ifm-color-primary-contrast-background:#102445;--ifm-color-primary-contrast-foreground:#ebf2fc;--ifm-color-secondary-contrast-background:#474748;--ifm-color-secondary-contrast-foreground:#fdfdfe;--ifm-color-success-contrast-background:#003100;--ifm-color-success-contrast-foreground:#e6f6e6;--ifm-color-info-contrast-background:#193c47;--ifm-color-info-contrast-foreground:#eef9fd;--ifm-color-warning-contrast-background:#4d3800;--ifm-color-warning-contrast-foreground:#fff8e6;--ifm-color-danger-contrast-background:#4b1113;--ifm-color-danger-contrast-foreground:#ffebec;--docsearch-text-color:#f5f6f7;--docsearch-container-background:#090a11cc;--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 #0304094d;--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 #494c6a80,0 -4px 8px 0 #0003;--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}#nprogress .bar{background:var(--docusaurus-progress-bar-color);height:2px;left:0;position:fixed;top:0;width:100%;z-index:1031}#nprogress .peg{box-shadow:0 0 10px var(--docusaurus-progress-bar-color),0 0 5px var(--docusaurus-progress-bar-color);height:100%;opacity:1;position:absolute;right:0;transform:rotate(3deg) translateY(-4px);width:100px}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:normal;font-weight:400;src:url(/assets/fonts/iAWriterQuattroSRegular-e47bd9556e7465f3cdca8944aebc1681.ttf) format("truetype")}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:normal;font-weight:700;src:url(/assets/fonts/iAWriterQuattroSBold-51c10554a8c6ab1d1e55af1528555325.ttf) format("truetype")}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:italic;font-weight:400;src:url(/assets/fonts/iAWriterQuattroSItalic-7584f678988e0674d9c5222d55b51786.ttf) format("truetype")}@font-face{font-display:swap;font-family:IamwriterquattroS;font-style:italic;font-weight:700;src:url(/assets/fonts/iAWriterQuattroSBoldItalic-6650b8cf6508d3ef099ecad91287ff48.ttf) format("truetype")}.hp-layout{margin:0 auto;max-width:42rem}.hp-hero{margin-top:3rem;width:18rem}.hp-header{font-size:1.875rem;line-height:2.25rem;margin-bottom:3.5rem;margin-top:6rem}.hp-list{gap:1rem;margin-left:-1rem}.hp-listitem{color:#00f;list-style-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTQuNDQzIDExLjI5M2EuOTQ5Ljk0OSAwIDAgMCAuODQxLS40NDRsNi4wNy05LjQxM2MuMDY5LS4xMS4xMTktLjIxNS4xNS0uMzE1YS45NDcuOTQ3IDAgMCAwIC4wNDktLjI4Ny44MTQuODE0IDAgMCAwLS4yMzMtLjYwMkEuODE0LjgxNCAwIDAgMCAxMC43MiAwYS44NjQuODY0IDAgMCAwLS40MzguMTAzIDEuMDYxIDEuMDYxIDAgMCAwLS4zMjguMzQ4TDQuNDE2IDkuMjM1IDEuNTcyIDUuNjEyYy0uMTkxLS4yNS0uNDI4LS4zNzYtLjcxLS4zNzYtLjI1MSAwLS40NTkuMDgtLjYyMy4yNC0uMTYuMTYtLjIzOS4zNi0uMjM5LjYwMSAwIC4xMS4wMi4yMTcuMDYyLjMyMS4wNC4xLjEwNy4yMDYuMTk4LjMxNWwzLjM0MyA0LjE1NmMuMjMyLjI4My41MTIuNDI0Ljg0LjQyNFoiIGZpbGw9IiMwMEYiLz48L3N2Zz4=)}.hp-listitem,.hp-text{font-size:1.25rem;line-height:1.75rem}.hp-text{margin-top:5rem}.hp-section{margin-top:9rem}.hp-section *{font-size:1.25rem}.hp-section-header{font-size:1.125rem;line-height:1.75rem;margin-bottom:1rem;text-decoration:underline}.cta-text{color:#00f}.footer-container{display:flex;flex-direction:column;font-size:1.25rem;gap:8rem;margin:0 auto;padding:4rem 0;width:70%}.footer-ls{display:flex;flex-wrap:wrap;gap:5rem 2rem;justify-content:space-around}.footer-ls-section{display:flex;flex-direction:column;gap:1.5rem}.footer-ls-section-title{color:#000;font-size:1.25rem;text-decoration-line:underline}.footer-ls-column{gap:1rem}.footer-cr,.footer-ls-column{display:flex;flex-direction:column}.footer-cr{align-items:center;gap:5rem;justify-content:center}.footer-cr p{text-align:center}.app-cta{border:2px solid #00f;border-radius:0;color:#00f;cursor:pointer;font-size:18px;padding:9px 15px;text-decoration:none}*,:root{--ifm-link-color:#00f;--ifm-color-primary:#00f;--ifm-color-primary-dark:#0000e6;--ifm-color-primary-darker:#0000d9;--ifm-color-primary-darkest:#0000b3;--ifm-color-primary-light:#1a1aff;--ifm-color-primary-lighter:#2626ff;--ifm-color-primary-lightest:#4d4dff;--privategpt-color:#5c00e2;--privategpt-width:56rem;--privategpt-max-width:100%;--privategpt-color-lightest:#f1e8fe}.bc-layout{min-height:calc(100vh - 60px);padding-bottom:6rem;padding-top:4rem}.bc-layout>*{transition:.5s}.bc-searchbar-icon{flex-shrink:0;height:22px;width:22.25px}.bc-thumbs-container{display:flex;margin-bottom:2rem}.bc-thumbs-button{align-items:center;border:1px solid var(--privategpt-color);border-radius:50%;display:flex;height:40px;justify-content:center;margin:.3rem;width:40px}.bc-quick-question:hover,.bc-thumbs-button:disabled,.bc-thumbs-button:hover{background-color:var(--privategpt-color-lightest)}.bc-thumbs-icon{border-radius:50%;height:30px;width:30px}.--loading,.bc-chat-embedded,.bc-quick-questions-panel,.bc-searchbar{display:flex;max-width:var(--privategpt-max-width);width:var(--privategpt-width)}.bc-searchbar{border-color:var(--privategpt-color);border-radius:50px;border-style:solid;border-width:1px;gap:1rem;padding:1rem;position:relative}.bc-input{border:none;color:#000;flex:1;font-size:medium;outline:0}.bc-beta-disclaimer,.bc-loader,.bc-quick-question{color:var(--privategpt-color)}.bc-input:disabled{background-color:#fff}.bc-reset-button{background:none;border:none;flex-shrink:0}.bc-reset-icon{height:15px}.bc-input::placeholder{color:gray;font-weight:400}.bc-chat-embedded{background-color:#fff;border-radius:1rem;flex-direction:column;height:-moz-fit-content;height:fit-content;padding:1rem;position:relative}.bc-quick-questions-panel{flex-wrap:wrap;gap:20px;justify-content:center;padding:30px 0}.bc-quick-question{background-color:#fff;border-color:var(--privategpt-color);border-radius:50px;border-width:1px;font-size:.85rem;padding:6px 15px}.bc-chat:first-child{margin-top:var(--ifm-paragraph-margin-bottom)}.--loading{align-items:center;justify-content:center;padding-top:1.5rem}.bc-loader{animation:1s linear infinite alternate a;border-radius:50%;display:block;height:12px;margin:15px auto;position:relative;width:12px}.bc-beta-disclaimer,.bc-chat-popup{max-width:var(--privategpt-max-width);width:var(--privategpt-width)}@keyframes a{0%{box-shadow:-38px -12px,-14px 0,14px 0,38px 0}33%{box-shadow:-38px 0,-14px -12px,14px 0,38px 0}66%{box-shadow:-38px 0,-14px 0,14px -12px,38px 0}to{box-shadow:-38px 0,-14px 0,14px 0,38px -12px}}.bc-chat-popup{align-items:center;background-color:#fff;border:2px solid #2a27e2;border-radius:10px;bottom:-200rem;display:flex;justify-content:center;padding:20px 30px;transition:.5s ease-in-out}.bc-beta-disclaimer{font-size:.65rem;margin-top:8px;text-align:center}svg{display:unset}.container{margin:unset;max-width:unset}.navbar_custom_item--button{background:none;border:none;margin-right:1rem;padding:0}.navbar_custom_item--image{height:15px}.utterances{max-width:100%}body:not(.navigation-with-keyboard) :not(input):focus{outline:0}#__docusaurus-base-url-issue-banner-container,.docSidebarContainer_YfHR,.sidebarLogo_isFc,.themedComponent_mlkZ,[data-theme=dark] .lightToggleIcon_pyhR,[data-theme=light] .darkToggleIcon_wfgR,html[data-announcement-bar-initially-dismissed=true] .announcementBar_mb4j{display:none}.skipToContent_fXgn{background-color:var(--ifm-background-surface-color);color:var(--ifm-color-emphasis-900);left:100%;padding:calc(var(--ifm-global-spacing)/2) var(--ifm-global-spacing);position:fixed;top:1rem;z-index:calc(var(--ifm-z-index-fixed) + 1)}.skipToContent_fXgn:focus{box-shadow:var(--ifm-global-shadow-md);left:1rem}.closeButton_CVFx{line-height:0;padding:0}.content_knG7{font-size:85%;padding:5px 0;text-align:center}.content_knG7 a{color:inherit;text-decoration:underline}.DocSearch-Container a,.sidebarItemLink_mo7H:hover{text-decoration:none}.announcementBar_mb4j{align-items:center;background-color:var(--ifm-color-white);border-bottom:1px solid var(--ifm-color-emphasis-100);color:var(--ifm-color-black);display:flex;height:var(--docusaurus-announcement-bar-height)}.terminalWindowHeader_o9Cs,.toggleButton_gllP:hover{background:var(--ifm-color-emphasis-200)}.announcementBarPlaceholder_vyr4{flex:0 0 10px}.announcementBarClose_gvF7{align-self:stretch;flex:0 0 30px}.announcementBarContent_xLdY{flex:1 1 auto}.toggle_vylO{height:2rem;width:2rem}.toggleButton_gllP{align-items:center;border-radius:50%;display:flex;height:100%;justify-content:center;transition:background var(--ifm-transition-fast);width:100%}.toggleButtonDisabled_aARS{cursor:not-allowed}.darkNavbarColorModeToggle_X3D1:hover{background:var(--ifm-color-gray-800)}[data-theme=dark] .themedComponent--dark_xIcU,[data-theme=light] .themedComponent--light_NVdE,html:not([data-theme]) .themedComponent--light_NVdE{display:initial}.iconExternalLink_nPIU{margin-left:.3rem}.dropdownNavbarItemMobile_S0Fm{cursor:pointer}.iconLanguage_nlXk{margin-right:5px;vertical-align:text-bottom}@supports selector(:has(*)){.navbarSearchContainer_Bca1:not(:has(>*)){display:none}}.navbarHideable_m1mJ{transition:transform var(--ifm-transition-fast) ease}.navbarHidden_jGov{transform:translate3d(0,calc(-100% - 2px),0)}.errorBoundaryError_a6uf{color:red;white-space:pre-wrap}.errorBoundaryFallback_VBag{color:red;padding:.55rem}.anchorWithStickyNavbar_LWe7{scroll-margin-top:calc(var(--ifm-navbar-height) + .5rem)}.anchorWithHideOnScrollNavbar_WYt5{scroll-margin-top:.5rem}.hash-link{opacity:0;padding-left:.5rem;transition:opacity var(--ifm-transition-fast);-webkit-user-select:none;user-select:none}.hash-link:before{content:"#"}.hash-link:focus,:hover>.hash-link{opacity:1}.mainWrapper_z2l0{display:flex;flex:1 0 auto;flex-direction:column}.docusaurus-mt-lg{margin-top:3rem}#__docusaurus{display:flex;flex-direction:column;min-height:100%}.terminalWindow_wGrl{border:3px solid var(--ifm-color-emphasis-200);border-radius:var(--ifm-global-radius);box-shadow:var(--ifm-global-shadow-lw);margin-bottom:var(--ifm-leading)}.terminalWindowHeader_o9Cs{align-items:center;display:flex;padding:.5rem 1rem}.row_Rn7G:after{clear:both;content:"";display:table}.buttons_IGLB{white-space:nowrap}.right_fWp9{align-self:center;width:10%}.terminalWindowAddressBar_X8fO{background-color:var(--ifm-background-color);border-radius:12.5px;color:var(--ifm-color-gray-800);flex:1 0;font:400 13px Arial,sans-serif;margin:0 1rem 0 .5rem;padding:5px 15px;-webkit-user-select:none;user-select:none}[data-theme=dark] .terminalWindowAddressBar_X8fO{color:var(--ifm-color-gray-300)}.dot_fGZE{background-color:#bbb;border-radius:50%;display:inline-block;height:12px;margin-right:6px;margin-top:4px;width:12px}.terminalWindowMenuIcon_rtOE{margin-left:auto}.bar_Ck8N{background-color:#aaa;display:block;height:3px;margin:3px 0;width:17px}.terminalWindowBody_tzdS{background-color:#292d3e;border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;color:#bfc7d5;padding:1rem}.terminalWindowBody_tzdS *{color:#bfc7d5}.cardContainer_fWXF{--ifm-link-color:var(--ifm-color-emphasis-800);--ifm-link-hover-color:var(--ifm-color-emphasis-700);--ifm-link-hover-decoration:none;border:1px solid var(--ifm-color-emphasis-200);box-shadow:0 1.5px 3px 0 #00000026;transition:all var(--ifm-transition-fast) ease;transition-property:border,box-shadow}.cardContainer_fWXF:hover{border-color:var(--ifm-color-primary);box-shadow:0 3px 6px 0 #0003}.cardTitle_rnsV{font-size:1.2rem}.cardDescription_PWke{font-size:.8rem}.searchQueryInput_u2C7,.searchVersionInput_m0Ui{background:var(--docsearch-searchbox-focus-background);border:2px solid var(--ifm-toc-border-color);border-radius:var(--ifm-global-radius);color:var(--docsearch-text-color);font:var(--ifm-font-size-base) var(--ifm-font-family-base);margin-bottom:.5rem;padding:.8rem;transition:border var(--ifm-transition-fast) ease;width:100%}.searchQueryInput_u2C7:focus,.searchVersionInput_m0Ui:focus{border-color:var(--docsearch-primary-color);outline:0}.searchQueryInput_u2C7::placeholder{color:var(--docsearch-muted-color)}.searchResultsColumn_JPFH{font-size:.9rem;font-weight:700}.algoliaLogo_rT1R{max-width:150px}.algoliaLogoPathFill_WdUC{fill:var(--ifm-font-color-base)}.searchResultItem_Tv2o{border-bottom:1px solid var(--ifm-toc-border-color);padding:1rem 0}.searchResultItemHeading_KbCB{font-weight:400;margin-bottom:0}.searchResultItemPath_lhe1{--ifm-breadcrumb-separator-size-multiplier:1;color:var(--ifm-color-content-secondary);font-size:.8rem}.searchResultItemSummary_AEaO{font-style:italic;margin:.5rem 0 0}.loadingSpinner_XVxU{animation:1s linear infinite b;border:.4em solid #eee;border-radius:50%;border-top:.4em solid var(--ifm-color-primary);height:3rem;margin:0 auto;width:3rem}@keyframes b{to{transform:rotate(1turn)}}.loader_vvXV{margin-top:2rem}.search-result-match{background:#ffd78e40;color:var(--docsearch-hit-color);padding:.09em 0}.tabList__CuJ{margin-bottom:var(--ifm-leading)}.tabItem_LNqP{margin-top:0!important}.sidebar_re4s{max-height:calc(100vh - var(--ifm-navbar-height) - 2rem);overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 2rem)}.sidebarItemTitle_pO2u{font-size:var(--ifm-h3-font-size);font-weight:var(--ifm-font-weight-bold)}.container_mt6G,.sidebarItemList_Yudw{font-size:.9rem}.sidebarItem__DBe{margin-top:.7rem}.sidebarItemLink_mo7H{color:var(--ifm-font-color-base);display:block}.sidebarItemLinkActive_I1ZP{color:var(--ifm-color-primary)!important}.title_f1Hy{font-size:3rem}.authorCol_Hf19{flex-grow:1!important;max-width:inherit!important}.imageOnlyAuthorRow_pa_O{display:flex;flex-flow:row wrap}.imageOnlyAuthorCol_G86a{margin-left:.3rem;margin-right:.3rem}.backToTopButton_sjWU{background-color:var(--ifm-color-emphasis-200);border-radius:50%;bottom:1.3rem;box-shadow:var(--ifm-global-shadow-lw);height:3rem;opacity:0;position:fixed;right:1.3rem;transform:scale(0);transition:all var(--ifm-transition-fast) var(--ifm-transition-timing-default);visibility:hidden;width:3rem;z-index:calc(var(--ifm-z-index-fixed) - 1)}.backToTopButton_sjWU:after{background-color:var(--ifm-color-emphasis-1000);content:" ";display:inline-block;height:100%;-webkit-mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;width:100%}.backToTopButtonShow_xfvO{opacity:1;transform:scale(1);visibility:visible}[data-theme=dark]:root{--docusaurus-collapse-button-bg:#ffffff0d;--docusaurus-collapse-button-bg-hover:#ffffff1a}.collapseSidebarButton_PEFL{display:none;margin:0}.menuExternalLink_NmtK{align-items:center}.docMainContainer_TBSr,.docRoot_UBD9{display:flex;width:100%}.docsWrapper_hBAB{display:flex;flex:1 0 auto}.DocSearch-Button,.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Button{background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;font-weight:500;height:36px;justify-content:space-between;padding:0 8px;-webkit-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:0}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Hit-Tree,.DocSearch-Hit-action,.DocSearch-Hit-icon,.DocSearch-Reset{stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Input,.DocSearch-Link{-webkit-appearance:none;font:inherit}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch--active{overflow:hidden!important}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Link{appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;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{appearance:none;background:#0000;border:0;color:var(--docsearch-text-color);flex:1;font-size:1.2em;height:100%;outline:0;padding:0 0 0 8px;width:80%}.DocSearch-Hit-action-button,.DocSearch-Reset{-webkit-appearance:none;border:0;cursor:pointer}.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-Container--Stalled .DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Cancel,.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator,.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset{animation:.1s ease-in forwards c;appearance:none;background:none;border-radius:50%;color:var(--docsearch-icon-color);padding:2px;right:0}.DocSearch-Help,.DocSearch-HitsFooter,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.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:#0000}.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{font-size:.9em;margin:0;-webkit-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{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}.DocSearch-Hit--deleting{opacity:0;transition:.25s linear}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:.25s linear .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;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)}.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{appearance:none;background:none;border-radius:50%;color:inherit;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon,.tocCollapsibleContent_vkbj a{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:background-color .1s ease-in}.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-Tree,.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] mark{color:var(--docsearch-hit-active-color)!important}.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:"» "}.DocSearch-Prefill{-webkit-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:0;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;user-select:none;width:100%;z-index:300}.DocSearch-Commands li,.DocSearch-Commands-Key{align-items:center;display:flex}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.buttonGroup__atx button,.codeBlockContainer_Ckt0{background:var(--prism-background-color);color:var(--prism-color)}@keyframes c{0%{opacity:0}to{opacity:1}}.DocSearch-Button{margin:0;transition:all var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.DocSearch-Container{z-index:calc(var(--ifm-z-index-fixed) + 1)}.codeBlockContainer_Ckt0{border-radius:var(--ifm-code-border-radius);box-shadow:var(--ifm-global-shadow-lw);margin-bottom:var(--ifm-leading)}.codeBlockContent_biex{border-radius:inherit;direction:ltr;position:relative}.codeBlockTitle_Ktv7{border-bottom:1px solid var(--ifm-color-emphasis-300);border-top-left-radius:inherit;border-top-right-radius:inherit;font-size:var(--ifm-code-font-size);font-weight:500;padding:.75rem var(--ifm-pre-padding)}.codeBlock_bY9V{--ifm-pre-background:var(--prism-background-color);margin:0;padding:0}.codeBlockTitle_Ktv7+.codeBlockContent_biex .codeBlock_bY9V{border-top-left-radius:0;border-top-right-radius:0}.codeBlockStandalone_MEMb{padding:0}.codeBlockLines_e6Vv{float:left;font:inherit;min-width:100%;padding:var(--ifm-pre-padding)}.codeBlockLinesWithNumbering_o6Pm{display:table;padding:var(--ifm-pre-padding) 0}.buttonGroup__atx{column-gap:.2rem;display:flex;position:absolute;right:calc(var(--ifm-pre-padding)/2);top:calc(var(--ifm-pre-padding)/2)}.buttonGroup__atx button{align-items:center;border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-global-radius);display:flex;line-height:0;opacity:0;padding:.4rem;transition:opacity var(--ifm-transition-fast) ease-in-out}.buttonGroup__atx button:focus-visible,.buttonGroup__atx button:hover{opacity:1!important}.theme-code-block:hover .buttonGroup__atx button{opacity:.4}.iconEdit_Z9Sw{margin-right:.3em;vertical-align:sub}:where(:root){--docusaurus-highlighted-code-line-bg:#484d5b}:where([data-theme=dark]){--docusaurus-highlighted-code-line-bg:#646464}.theme-code-block-highlighted-line{background-color:var(--docusaurus-highlighted-code-line-bg);display:block;margin:0 calc(var(--ifm-pre-padding)*-1);padding:0 var(--ifm-pre-padding)}.codeLine_lJS_{counter-increment:a;display:table-row}.codeLineNumber_Tfdd{background:var(--ifm-pre-background);display:table-cell;left:0;overflow-wrap:normal;padding:0 var(--ifm-pre-padding);position:sticky;text-align:right;width:1%}.codeLineNumber_Tfdd:before{content:counter(a);opacity:.4}.theme-code-block-highlighted-line .codeLineNumber_Tfdd:before{opacity:.8}.codeLineContent_feaV{padding-right:var(--ifm-pre-padding)}.tag_zVej{border:1px solid var(--docusaurus-tag-list-border);transition:border var(--ifm-transition-fast)}.tag_zVej:hover{--docusaurus-tag-list-border:var(--ifm-link-color);text-decoration:none}.tagRegular_sFm0{border-radius:var(--ifm-global-radius);font-size:90%;padding:.2rem .5rem .3rem}.tagWithCount_h2kH{align-items:center;border-left:0;display:flex;padding:0 .5rem 0 1rem;position:relative}.tagWithCount_h2kH:after,.tagWithCount_h2kH:before{border:1px solid var(--docusaurus-tag-list-border);content:"";position:absolute;top:50%;transition:inherit}.tagWithCount_h2kH:before{border-bottom:0;border-right:0;height:1.18rem;right:100%;transform:translate(50%,-50%) rotate(-45deg);width:1.18rem}.tagWithCount_h2kH:after{border-radius:50%;height:.5rem;left:0;transform:translateY(-50%);width:.5rem}.tagWithCount_h2kH span{background:var(--ifm-color-secondary);border-radius:var(--ifm-global-radius);color:var(--ifm-color-black);font-size:.7rem;line-height:1.2;margin-left:.3rem;padding:.1rem .4rem}.theme-code-block:hover .copyButtonCopied_obH4{opacity:1!important}.copyButtonIcons_eSgA{height:1.125rem;position:relative;width:1.125rem}.copyButtonIcon_y97N,.copyButtonSuccessIcon_LjdS{fill:currentColor;height:inherit;left:0;opacity:inherit;position:absolute;top:0;transition:all var(--ifm-transition-fast) ease;width:inherit}.copyButtonSuccessIcon_LjdS{color:#00d600;left:50%;opacity:0;top:50%;transform:translate(-50%,-50%) scale(.33)}.copyButtonCopied_obH4 .copyButtonIcon_y97N{opacity:0;transform:scale(.33)}.copyButtonCopied_obH4 .copyButtonSuccessIcon_LjdS{opacity:1;transform:translate(-50%,-50%) scale(1);transition-delay:75ms}.tags_jXut{display:inline}.tag_QGVx{display:inline-block;margin:0 .4rem .5rem 0}.lastUpdated_vwxv{font-size:smaller;font-style:italic;margin-top:.2rem}.tocCollapsibleButton_TO0P{align-items:center;display:flex;font-size:inherit;justify-content:space-between;padding:.4rem .8rem;width:100%}.tocCollapsibleButton_TO0P:after{background:var(--ifm-menu-link-sublist-icon) 50% 50%/2rem 2rem no-repeat;content:"";filter:var(--ifm-menu-link-sublist-icon-filter);height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast);width:1.25rem}.tocCollapsibleButtonExpanded_MG3E:after,.tocCollapsibleExpanded_sAul{transform:none}.tocCollapsible_ETCw{background-color:var(--ifm-menu-color-background-active);border-radius:var(--ifm-global-radius);margin:1rem 0}.tocCollapsibleContent_vkbj>ul{border-left:none;border-top:1px solid var(--ifm-color-emphasis-300);font-size:15px;padding:.2rem 0}.tocCollapsibleContent_vkbj ul li{margin:.4rem .8rem}.wordWrapButtonIcon_Bwma{height:1.2rem;width:1.2rem}.wordWrapButtonEnabled_EoeP .wordWrapButtonIcon_Bwma{color:var(--ifm-color-primary)}.details_lb9f{--docusaurus-details-summary-arrow-size:0.38rem;--docusaurus-details-transition:transform 200ms ease;--docusaurus-details-decoration-color:grey}.details_lb9f>summary{cursor:pointer;list-style:none;padding-left:1rem;position:relative}.details_lb9f>summary::-webkit-details-marker{display:none}.details_lb9f>summary:before{border-color:#0000 #0000 #0000 var(--docusaurus-details-decoration-color);border-style:solid;border-width:var(--docusaurus-details-summary-arrow-size);content:"";left:0;position:absolute;top:.45rem;transform:rotate(0);transform-origin:calc(var(--docusaurus-details-summary-arrow-size)/2) 50%;transition:var(--docusaurus-details-transition)}.collapsibleContent_i85q{border-top:1px solid var(--docusaurus-details-decoration-color);margin-top:1rem;padding-top:1rem}.details_b_Ee{--docusaurus-details-decoration-color:var(--ifm-alert-border-color);--docusaurus-details-transition:transform var(--ifm-transition-fast) ease;border:1px solid var(--ifm-alert-border-color);margin:0 0 var(--ifm-spacing-vertical)}.containsTaskList_mC6p{list-style:none}.img_ev3q{height:auto}.admonition_xJq3{margin-bottom:1em}.admonitionHeading_Gvgb{font:var(--ifm-heading-font-weight) var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family)}.admonitionHeading_Gvgb:not(:last-child){margin-bottom:.3rem}.admonitionHeading_Gvgb code{text-transform:none}.admonitionIcon_Rf37{display:inline-block;margin-right:.4em;vertical-align:middle}.admonitionIcon_Rf37 svg{fill:var(--ifm-alert-foreground-color);display:inline-block;height:1.6em;width:1.6em}.blogPostFooterDetailsFull_mRVl{flex-direction:column}.tableOfContents_bqdL{max-height:calc(100vh - var(--ifm-navbar-height) - 2rem);overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 1rem)}.breadcrumbHomeIcon_YNFT{height:1.1rem;position:relative;top:1px;vertical-align:top;width:1.1rem}.breadcrumbsContainer_Z_bl{--ifm-breadcrumb-size-multiplier:0.8;margin-bottom:.8rem}.title_kItE{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-leading)*1.25)}.list_eTzJ article:last-child{margin-bottom:0!important}@media screen and (min-width:769px) and (max-width:997px){.navbar_custom_item--button{margin-right:11rem}}@media (min-width:997px){.collapseSidebarButton_PEFL,.expandButton_TmdG{background-color:var(--docusaurus-collapse-button-bg)}:root{--docusaurus-announcement-bar-height:30px}.announcementBarClose_gvF7,.announcementBarPlaceholder_vyr4{flex-basis:50px}.navbarSearchContainer_Bca1{padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.collapseSidebarButton_PEFL{border:1px solid var(--ifm-toc-border-color);border-radius:0;bottom:0;display:block!important;height:40px;position:sticky}.collapseSidebarButtonIcon_kv0_{margin-top:4px;transform:rotate(180deg)}.expandButtonIcon_i1dp,[dir=rtl] .collapseSidebarButtonIcon_kv0_{transform:rotate(0)}.collapseSidebarButton_PEFL:focus,.collapseSidebarButton_PEFL:hover,.expandButton_TmdG:focus,.expandButton_TmdG:hover{background-color:var(--docusaurus-collapse-button-bg-hover)}.menuHtmlItem_M9Kj{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu_SIkG{flex-grow:1;padding:.5rem}@supports (scrollbar-gutter:stable){.menu_SIkG{padding:.5rem 0 .5rem .5rem;scrollbar-gutter:stable}}.menuWithAnnouncementBar_GW3s{margin-bottom:var(--docusaurus-announcement-bar-height)}.sidebar_njMd{display:flex;flex-direction:column;height:100%;padding-top:var(--ifm-navbar-height);width:var(--doc-sidebar-width)}.sidebarWithHideableNavbar_wUlq{padding-top:0}.sidebarHidden_VK0M{opacity:0;visibility:hidden}.sidebarLogo_isFc{align-items:center;color:inherit!important;display:flex!important;margin:0 var(--ifm-navbar-padding-horizontal);max-height:var(--ifm-navbar-height);min-height:var(--ifm-navbar-height);text-decoration:none!important}.sidebarLogo_isFc img{height:2rem;margin-right:.5rem}.expandButton_TmdG{align-items:center;display:flex;height:100%;justify-content:center;position:absolute;right:0;top:0;transition:background-color var(--ifm-transition-fast) ease;width:100%}[dir=rtl] .expandButtonIcon_i1dp{transform:rotate(180deg)}.docSidebarContainer_YfHR{border-right:1px solid var(--ifm-toc-border-color);-webkit-clip-path:inset(0);clip-path:inset(0);display:block;margin-top:calc(var(--ifm-navbar-height)*-1);transition:width var(--ifm-transition-fast) ease;width:var(--doc-sidebar-width);will-change:width}.docSidebarContainerHidden_DPk8{cursor:pointer;width:var(--doc-sidebar-hidden-width)}.sidebarViewport_aRkj{height:100%;max-height:100vh;position:sticky;top:0}.docMainContainer_TBSr{flex-grow:1;max-width:calc(100% - var(--doc-sidebar-width))}.docMainContainerEnhanced_lQrH{max-width:calc(100% - var(--doc-sidebar-hidden-width))}.docItemWrapperEnhanced_JWYK{max-width:calc(var(--ifm-container-width) + var(--doc-sidebar-width))!important}.lastUpdated_vwxv{text-align:right}.tocMobile_ITEo{display:none}.docItemCol_VOVn,.generatedIndexPage_vN6x{max-width:75%!important}.list_eTzJ article:nth-last-child(-n+2){margin-bottom:0!important}}@media screen and (min-width:1280px){.footer-container{width:55%}}@media (min-width:1440px){.container{max-width:var(--ifm-container-width-xl)}}@media (max-width:996px){.col{--ifm-col-width:100%;flex-basis:var(--ifm-col-width);margin-left:0}.footer{--ifm-footer-padding-horizontal:0}.colorModeToggle_DEke,.footer__link-separator,.navbar__item,.sidebar_re4s,.tableOfContents_bqdL{display:none}.footer__col{margin-bottom:calc(var(--ifm-spacing-vertical)*3)}.footer__link-item{display:block}.hero{padding-left:0;padding-right:0}.navbar>.container,.navbar>.container-fluid{padding:0}.navbar__toggle{display:inherit}.navbar__search-input{width:9rem}.pills--block,.tabs--block{flex-direction:column}.navbarSearchContainer_Bca1{position:absolute;right:var(--ifm-navbar-padding-horizontal)}.docItemContainer_F8PC{padding:0 .3rem}}@media only screen and (max-width:996px){.searchQueryColumn_RTkw,.searchResultsColumn_JPFH{max-width:60%!important}.searchLogoColumn_rJIA,.searchVersionColumn_ypXd{max-width:40%!important}.searchLogoColumn_rJIA{padding-left:0!important}}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder,.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%;max-height:calc(var(--docsearch-vh,1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.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-Cancel{-webkit-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:0;overflow:hidden;padding:0;-webkit-user-select:none;user-select:none;white-space:nowrap}}@media screen and (max-width:768px){.navbar_custom_item--button{margin-right:3rem}}@media screen and (max-width:767px){.hp-layout{max-width:90%}}@media (max-width:576px){.markdown h1:first-child{--ifm-h1-font-size:2rem}.markdown>h2{--ifm-h2-font-size:1.5rem}.markdown>h3{--ifm-h3-font-size:1.25rem}.title_f1Hy{font-size:2rem}}@media screen and (max-width:576px){.searchQueryColumn_RTkw{max-width:100%!important}.searchVersionColumn_ypXd{max-width:100%!important;padding-left:var(--ifm-spacing-horizontal)!important}}@media (hover:hover){.backToTopButton_sjWU:hover{background-color:var(--ifm-color-emphasis-300)}}@media (pointer:fine){.thin-scrollbar{scrollbar-width:thin}.thin-scrollbar::-webkit-scrollbar{height:var(--ifm-scrollbar-size);width:var(--ifm-scrollbar-size)}.thin-scrollbar::-webkit-scrollbar-track{background:var(--ifm-scrollbar-track-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb{background:var(--ifm-scrollbar-thumb-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb:hover{background:var(--ifm-scrollbar-thumb-hover-background-color)}}@media (prefers-reduced-motion:reduce){:root{--ifm-transition-fast:0ms;--ifm-transition-slow:0ms}}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{stroke-width:var(--docsearch-icon-stroke-width);animation:none;-webkit-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0}.DocSearch-Hit--deleting,.DocSearch-Hit--favoriting{transition:none}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:#0003;transition:none}}@media print{.announcementBar_mb4j,.footer,.menu,.navbar,.pagination-nav,.table-of-contents,.tocMobile_ITEo{display:none}.tabs{page-break-inside:avoid}.codeBlockLines_e6Vv{white-space:pre-wrap}} \ No newline at end of file diff --git a/assets/js/021264af.30a8a515.js b/assets/js/021264af.30a8a515.js deleted file mode 100644 index 3e6f23421..000000000 --- a/assets/js/021264af.30a8a515.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[5033],{2784:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>l,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var r=n(5893),s=n(1151);const i={},o="Advanced uses of the Register object",a={id:"going-deeper/register",title:"Advanced uses of the Register object",description:"The Register object is a built-in object that is automatically injected by the framework into all command or event handlers to let users interact with the execution context. It can be used for a variety of purposes, including:",source:"@site/docs/10_going-deeper/register.mdx",sourceDirName:"10_going-deeper",slug:"/going-deeper/register",permalink:"/going-deeper/register",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/register.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Environments",permalink:"/going-deeper/environment-configuration"},next:{title:"Configuring Infrastructure Providers",permalink:"/going-deeper/infrastructure-providers"}},c={},d=[{value:"Registering events",id:"registering-events",level:2},{value:"Manually flush the events",id:"manually-flush-the-events",level:2},{value:"Access the current signed in user",id:"access-the-current-signed-in-user",level:2},{value:"Command-specific features",id:"command-specific-features",level:2},{value:"Access the request context",id:"access-the-request-context",level:3},{value:"Alter the HTTP response headers",id:"alter-the-http-response-headers",level:3}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"advanced-uses-of-the-register-object",children:"Advanced uses of the Register object"}),"\n",(0,r.jsx)(t.p,{children:"The Register object is a built-in object that is automatically injected by the framework into all command or event handlers to let users interact with the execution context. It can be used for a variety of purposes, including:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:"Registering events to be emitted at the end of the command or event handler"}),"\n",(0,r.jsx)(t.li,{children:"Manually flush the events to be persisted synchronously to the event store"}),"\n",(0,r.jsx)(t.li,{children:"Access the current signed in user, their roles and other claims included in their JWT token"}),"\n",(0,r.jsx)(t.li,{children:"In a command: Access the request context or alter the HTTP response headers"}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"registering-events",children:"Registering events"}),"\n",(0,r.jsxs)(t.p,{children:["When handling a command or event, you can use the Register object to register one or more events that will be emitted when the command or event handler is completed. Events are registered using the ",(0,r.jsx)(t.code,{children:"register.events()"})," method, which takes one or more events as arguments. For example:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n // Do some work...\n register.events(new OrderConfirmed(this.orderID))\n // Do more work...\n}\n"})}),"\n",(0,r.jsx)(t.p,{children:"In this example, we're registering an OrderConfirmed event to be persisted to the event store when the handler finishes. You can also register multiple events by passing them as separate arguments to the register.events() method:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n // Do some work...\n register.events(\n new OrderConfirmed(this.orderID),\n new OrderShipped(this.orderID)\n )\n // Do more work...\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["It's worth noting that events registered with ",(0,r.jsx)(t.code,{children:"register.events()"})," aren't immediately persisted to the event store. Instead, they're stored in memory until the command or event handler finishes executing. To force the events to be persisted immediately, you can call the ",(0,r.jsx)(t.code,{children:"register.flush()"})," method that is described in the next section."]}),"\n",(0,r.jsx)(t.h2,{id:"manually-flush-the-events",children:"Manually flush the events"}),"\n",(0,r.jsxs)(t.p,{children:["As mentioned in the previous section, events registered with ",(0,r.jsx)(t.code,{children:"register.events()"})," aren't immediately persisted to the event store. Instead, they're stored in memory until the command or event handler finishes its execution, but this doesn't work in all situations, sometimes it's useful to store partial updates of a longer process, and some scenarios could accept partial successes. To force the events to be persisted and wait for the database to confirm the write, you can use the ",(0,r.jsx)(t.code,{children:"register.flush()"})," method."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"register.flush()"})," method takes no arguments and returns a promise that resolves when the events have been successfully persisted to the event store. For example:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n // Do some work...\n register.events(new OrderConfirmed(this.orderID))\n await register.flush()\n const mailID = await sendConfirmationEmail(this.orderID)\n register.events(new MailSent(this.orderID, mailID))\n // Do more work...\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["In this example, we're calling ",(0,r.jsx)(t.code,{children:"register.flush()"})," after registering an ",(0,r.jsx)(t.code,{children:"OrderConfirmed"})," event to ensure that it's persisted to the event store before continuing with the rest of the handler logic. In this way, even if an error happens while sending the confirmation email, the order will be persisted."]}),"\n",(0,r.jsx)(t.h2,{id:"access-the-current-signed-in-user",children:"Access the current signed in user"}),"\n",(0,r.jsxs)(t.p,{children:["When handling a command or event, you can use the injected ",(0,r.jsx)(t.code,{children:"Register"})," object to access the currently signed-in user as well as any metadata included in their JWT token like their roles or other claims (the specific claims will depend on the specific auth provider used). To do this, you can use the ",(0,r.jsx)(t.code,{children:"currentUser"})," property. This property is an instance of the ",(0,r.jsx)(t.code,{children:"UserEnvelope"})," class, which has the following properties:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"export interface UserEnvelope {\n id?: string // An optional identifier of the user\n username: string // The unique username of the current user\n roles: Array // The list of role names assigned to this user\n claims: Record // An object containing the claims included in the body of the JWT token\n header?: Record // An object containing the headers of the JWT token for further verification\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["For example, to access the username of the currently signed-in user, you can use the ",(0,r.jsx)(t.code,{children:"currentUser.username"})," property:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n console.log(`The currently signed-in user is ${register.currentUser?.username}`)\n}\n\n// Output: The currently signed-in user is john.doe\n"})}),"\n",(0,r.jsx)(t.h2,{id:"command-specific-features",children:"Command-specific features"}),"\n",(0,r.jsx)(t.p,{children:"The command handlers are executed as part of a GraphQL mutation request, so they have access to a few additional features that are specific to commands that can be used to access the request context or alter the HTTP response headers."}),"\n",(0,r.jsx)(t.h3,{id:"access-the-request-context",children:"Access the request context"}),"\n",(0,r.jsxs)(t.p,{children:["The request context is injected in the command handler as part of the register command and you can access it using the ",(0,r.jsx)(t.code,{children:"context"})," property. This property is an instance of the ",(0,r.jsx)(t.code,{children:"ContextEnvelope"})," interface, which has the following properties:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"export interface ContextEnvelope {\n /** Decoded request header and body */\n request: {\n headers: unknown\n body: unknown\n }\n /** Provider-dependent raw request context object */\n rawContext: unknown\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"request"})," property exposes a normalized version of the request headers and body that can be used regardless the provider. We recommend using this property instead of the ",(0,r.jsx)(t.code,{children:"rawContext"})," property, as it will be more portable across providers."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"rawContext"})," property exposes the full raw request context as it comes in the original request, so it will depend on the underlying provider used. For instance, in AWS, it will be ",(0,r.jsx)(t.a,{href:"https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html",children:"a lambda context object"}),", while in Azure it will be ",(0,r.jsx)(t.a,{href:"https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object",children:"an Azure Functions context object"}),"."]}),"\n",(0,r.jsx)(t.h3,{id:"alter-the-http-response-headers",children:"Alter the HTTP response headers"}),"\n",(0,r.jsxs)(t.p,{children:["Finally, you can use the ",(0,r.jsx)(t.code,{children:"responseHeaders"})," property to alter the HTTP response headers that will be sent back to the client. This property is a plain Typescript object which is initialized with the default headers. You can add, remove or modify any of the headers by using the standard object methods:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n register.responseHeaders['X-My-Header'] = 'My custom header'\n register.responseHeaders['X-My-Other-Header'] = 'My other custom header'\n delete register.responseHeaders['X-My-Other-Header']\n}\n"})})]})}function l(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>o});var r=n(7294);const s={},i=r.createContext(s);function o(e){const t=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/021264af.eb140c15.js b/assets/js/021264af.eb140c15.js new file mode 100644 index 000000000..89ff2f1f6 --- /dev/null +++ b/assets/js/021264af.eb140c15.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[5033],{2784:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>l,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var r=n(5893),s=n(1151);const i={},o="Advanced uses of the Register object",a={id:"going-deeper/register",title:"Advanced uses of the Register object",description:"The Register object is a built-in object that is automatically injected by the framework into all command or event handlers to let users interact with the execution context. It can be used for a variety of purposes, including:",source:"@site/docs/10_going-deeper/register.mdx",sourceDirName:"10_going-deeper",slug:"/going-deeper/register",permalink:"/going-deeper/register",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/register.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Environments",permalink:"/going-deeper/environment-configuration"},next:{title:"Configuring Infrastructure Providers",permalink:"/going-deeper/infrastructure-providers"}},c={},d=[{value:"Registering events",id:"registering-events",level:2},{value:"Manually flush the events",id:"manually-flush-the-events",level:2},{value:"Access the current signed in user",id:"access-the-current-signed-in-user",level:2},{value:"Command-specific features",id:"command-specific-features",level:2},{value:"Access the request context",id:"access-the-request-context",level:3},{value:"Alter the HTTP response headers",id:"alter-the-http-response-headers",level:3}];function h(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"advanced-uses-of-the-register-object",children:"Advanced uses of the Register object"}),"\n",(0,r.jsx)(t.p,{children:"The Register object is a built-in object that is automatically injected by the framework into all command or event handlers to let users interact with the execution context. It can be used for a variety of purposes, including:"}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:"Registering events to be emitted at the end of the command or event handler"}),"\n",(0,r.jsx)(t.li,{children:"Manually flush the events to be persisted synchronously to the event store"}),"\n",(0,r.jsx)(t.li,{children:"Access the current signed in user, their roles and other claims included in their JWT token"}),"\n",(0,r.jsx)(t.li,{children:"In a command: Access the request context or alter the HTTP response headers"}),"\n"]}),"\n",(0,r.jsx)(t.h2,{id:"registering-events",children:"Registering events"}),"\n",(0,r.jsxs)(t.p,{children:["When handling a command or event, you can use the Register object to register one or more events that will be emitted when the command or event handler is completed. Events are registered using the ",(0,r.jsx)(t.code,{children:"register.events()"})," method, which takes one or more events as arguments. For example:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n // Do some work...\n register.events(new OrderConfirmed(this.orderID))\n // Do more work...\n}\n"})}),"\n",(0,r.jsx)(t.p,{children:"In this example, we're registering an OrderConfirmed event to be persisted to the event store when the handler finishes. You can also register multiple events by passing them as separate arguments to the register.events() method:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n // Do some work...\n register.events(\n new OrderConfirmed(this.orderID),\n new OrderShipped(this.orderID)\n )\n // Do more work...\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["It's worth noting that events registered with ",(0,r.jsx)(t.code,{children:"register.events()"})," aren't immediately persisted to the event store. Instead, they're stored in memory until the command or event handler finishes executing. To force the events to be persisted immediately, you can call the ",(0,r.jsx)(t.code,{children:"register.flush()"})," method that is described in the next section."]}),"\n",(0,r.jsx)(t.h2,{id:"manually-flush-the-events",children:"Manually flush the events"}),"\n",(0,r.jsxs)(t.p,{children:["As mentioned in the previous section, events registered with ",(0,r.jsx)(t.code,{children:"register.events()"})," aren't immediately persisted to the event store. Instead, they're stored in memory until the command or event handler finishes its execution, but this doesn't work in all situations, sometimes it's useful to store partial updates of a longer process, and some scenarios could accept partial successes. To force the events to be persisted and wait for the database to confirm the write, you can use the ",(0,r.jsx)(t.code,{children:"register.flush()"})," method."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"register.flush()"})," method takes no arguments and returns a promise that resolves when the events have been successfully persisted to the event store. For example:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n // Do some work...\n register.events(new OrderConfirmed(this.orderID))\n await register.flush()\n const mailID = await sendConfirmationEmail(this.orderID)\n register.events(new MailSent(this.orderID, mailID))\n // Do more work...\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["In this example, we're calling ",(0,r.jsx)(t.code,{children:"register.flush()"})," after registering an ",(0,r.jsx)(t.code,{children:"OrderConfirmed"})," event to ensure that it's persisted to the event store before continuing with the rest of the handler logic. In this way, even if an error happens while sending the confirmation email, the order will be persisted."]}),"\n",(0,r.jsx)(t.h2,{id:"access-the-current-signed-in-user",children:"Access the current signed in user"}),"\n",(0,r.jsxs)(t.p,{children:["When handling a command or event, you can use the injected ",(0,r.jsx)(t.code,{children:"Register"})," object to access the currently signed-in user as well as any metadata included in their JWT token like their roles or other claims (the specific claims will depend on the specific auth provider used). To do this, you can use the ",(0,r.jsx)(t.code,{children:"currentUser"})," property. This property is an instance of the ",(0,r.jsx)(t.code,{children:"UserEnvelope"})," class, which has the following properties:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"export interface UserEnvelope {\n id?: string // An optional identifier of the user\n username: string // The unique username of the current user\n roles: Array // The list of role names assigned to this user\n claims: Record // An object containing the claims included in the body of the JWT token\n header?: Record // An object containing the headers of the JWT token for further verification\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["For example, to access the username of the currently signed-in user, you can use the ",(0,r.jsx)(t.code,{children:"currentUser.username"})," property:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n console.log(`The currently signed-in user is ${register.currentUser?.username}`)\n}\n\n// Output: The currently signed-in user is john.doe\n"})}),"\n",(0,r.jsx)(t.h2,{id:"command-specific-features",children:"Command-specific features"}),"\n",(0,r.jsx)(t.p,{children:"The command handlers are executed as part of a GraphQL mutation request, so they have access to a few additional features that are specific to commands that can be used to access the request context or alter the HTTP response headers."}),"\n",(0,r.jsx)(t.h3,{id:"access-the-request-context",children:"Access the request context"}),"\n",(0,r.jsxs)(t.p,{children:["The request context is injected in the command handler as part of the register command and you can access it using the ",(0,r.jsx)(t.code,{children:"context"})," property. This property is an instance of the ",(0,r.jsx)(t.code,{children:"ContextEnvelope"})," interface, which has the following properties:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"export interface ContextEnvelope {\n /** Decoded request header and body */\n request: {\n headers: unknown\n body: unknown\n }\n /** Provider-dependent raw request context object */\n rawContext: unknown\n}\n"})}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"request"})," property exposes a normalized version of the request headers and body that can be used regardless the provider. We recommend using this property instead of the ",(0,r.jsx)(t.code,{children:"rawContext"})," property, as it will be more portable across providers."]}),"\n",(0,r.jsxs)(t.p,{children:["The ",(0,r.jsx)(t.code,{children:"rawContext"})," property exposes the full raw request context as it comes in the original request, so it will depend on the underlying provider used. For instance, in AWS, it will be ",(0,r.jsx)(t.a,{href:"https://docs.aws.amazon.com/lambda/latest/dg/nodejs-context.html",children:"a lambda context object"}),", while in Azure it will be ",(0,r.jsx)(t.a,{href:"https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object",children:"an Azure Functions context object"}),"."]}),"\n",(0,r.jsx)(t.h3,{id:"alter-the-http-response-headers",children:"Alter the HTTP response headers"}),"\n",(0,r.jsxs)(t.p,{children:["Finally, you can use the ",(0,r.jsx)(t.code,{children:"responseHeaders"})," property to alter the HTTP response headers that will be sent back to the client. This property is a plain Typescript object which is initialized with the default headers. You can add, remove or modify any of the headers by using the standard object methods:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"public async handle(register: Register): Promise {\n register.responseHeaders['X-My-Header'] = 'My custom header'\n register.responseHeaders['X-My-Other-Header'] = 'My other custom header'\n delete register.responseHeaders['X-My-Other-Header']\n}\n"})})]})}function l(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>o});var r=n(7294);const s={},i=r.createContext(s);function o(e){const t=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0350e44c.29c97fef.js b/assets/js/0350e44c.29c97fef.js deleted file mode 100644 index 0679b9e9f..000000000 --- a/assets/js/0350e44c.29c97fef.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[8946],{4380:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>r,toc:()=>l});var s=n(5893),o=n(1151);const i={},a="Testing",r={id:"going-deeper/testing",title:"Testing",description:"Booster applications are fully tested by default. This means that you can be sure that your application will work as expected. However, you can also write your own tests to check that your application behaves as you expect. In this section, we will leave some recommendations on how to test your Booster application.",source:"@site/docs/10_going-deeper/testing.md",sourceDirName:"10_going-deeper",slug:"/going-deeper/testing",permalink:"/going-deeper/testing",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/testing.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"sensor-health",permalink:"/going-deeper/health/sensor-health"},next:{title:"Migrations",permalink:"/going-deeper/data-migrations"}},c={},l=[{value:"Testing Booster applications",id:"testing-booster-applications",level:2},{value:"Testing with sinon-chai",id:"testing-with-sinon-chai",level:3},{value:"Recommended files",id:"recommended-files",level:3},{value:"Framework integration tests",id:"framework-integration-tests",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"testing",children:"Testing"}),"\n",(0,s.jsx)(t.p,{children:"Booster applications are fully tested by default. This means that you can be sure that your application will work as expected. However, you can also write your own tests to check that your application behaves as you expect. In this section, we will leave some recommendations on how to test your Booster application."}),"\n",(0,s.jsx)(t.h2,{id:"testing-booster-applications",children:"Testing Booster applications"}),"\n",(0,s.jsxs)(t.p,{children:["To properly test a Booster application, you should create a ",(0,s.jsx)(t.code,{children:"test"})," folder at the same level as the ",(0,s.jsx)(t.code,{children:"src"})," one. Apart from that, tests' names should have the ",(0,s.jsx)(t.code,{children:".test.ts"})," format."]}),"\n",(0,s.jsxs)(t.p,{children:["When a Booster application is generated, you will have a script in a ",(0,s.jsx)(t.code,{children:"package.json"})," like this:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:'"scripts": {\n "test": "nyc --extension .ts mocha --forbid-only \\"test/**/*.test.ts\\""\n}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["The only thing that you should add to this line are the ",(0,s.jsx)(t.code,{children:"AWS_SDK_LOAD_CONFIG=true"})," and ",(0,s.jsx)(t.code,{children:"BOOSTER_ENV=test"})," environment variables, so the script will look like this:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:'"scripts": {\n "test": "AWS_SDK_LOAD_CONFIG=true BOOSTER_ENV=test nyc --extension .ts mocha --forbid-only \\"test/**/*.test.ts\\""\n}\n'})}),"\n",(0,s.jsxs)(t.h3,{id:"testing-with-sinon-chai",children:["Testing with ",(0,s.jsx)(t.code,{children:"sinon-chai"})]}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.code,{children:"BoosterConfig"})," can be accessed through the ",(0,s.jsx)(t.code,{children:"Booster.config"})," on any part of a Booster application. To properly mock it for your objective, we really recommend to use sinon ",(0,s.jsx)(t.code,{children:"replace"})," method, after configuring your ",(0,s.jsx)(t.code,{children:"Booster.config"})," as desired."]}),"\n",(0,s.jsxs)(t.p,{children:['In the example below, we add 2 "empty" read-models, since we are iterating ',(0,s.jsx)(t.code,{children:"Booster.config.readModels"})," from a command handler:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// Test\nimport { replace } from 'sinon'\n\nconst config = new BoosterConfig('test')\nconfig.appName = 'testing-time'\nconfig.providerPackage = '@boostercloud/framework-provider-aws'\nconfig.readModels['WoW'] = {} as ReadModelMetadata\nconfig.readModels['Amazing'] = {} as ReadModelMetadata\nreplace(Booster, 'config', config)\n\nconst spyMyCall = spy(MyCommand, 'myCall')\nconst command = new MyCommand('1', true)\nconst register = new Register('request-id-1')\nconst registerSpy = spy(register, 'events')\nawait MyCommand.handle(command, register)\n\nexpect(spyMyCall).to.have.been.calledOnceWithExactly('WoW')\nexpect(spyMyCall).to.have.been.calledOnceWithExactly('Amazing')\nexpect(registerSpy).to.have.been.calledOnceWithExactly(new MyEvent('1', 'WoW'))\nexpect(registerSpy).to.have.been.calledOnceWithExactly(new MyEvent('1', 'Amazing'))\n"})}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// Example code\npublic static async handle(command: MyCommand, register: Register): Promise {\n const readModels = Booster.config.readModels\n for (const readModelName in readModels) {\n myCall(readModelName)\n register.events(new MyEvent(command.ID, readModelName))\n }\n}\n"})}),"\n",(0,s.jsx)(t.h3,{id:"recommended-files",children:"Recommended files"}),"\n",(0,s.jsx)(t.p,{children:"These are some files that might help you speed up your testing with Booster."}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// /test/expect.ts\nimport * as chai from 'chai'\n\nchai.use(require('sinon-chai'))\nchai.use(require('chai-as-promised'))\n\nexport const expect = chai.expect\n"})}),"\n",(0,s.jsxs)(t.p,{children:["This ",(0,s.jsx)(t.code,{children:"expect"})," method will help you with some more additional methods like ",(0,s.jsx)(t.code,{children:"expect().to.have.been.calledOnceWithExactly()"})]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-yaml",children:"# /.mocharc.yml\ndiff: true\nrequire: 'ts-node/register'\nextension:\n - ts\npackage: './package.json'\nrecursive: true\nreporter: 'spec'\ntimeout: 5000\nfull-trace: true\nbail: true\n"})}),"\n",(0,s.jsx)(t.h2,{id:"framework-integration-tests",children:"Framework integration tests"}),"\n",(0,s.jsxs)(t.p,{children:["Booster framework integration tests package is used to test the Booster project itself, but it is also an example of how a Booster application could be tested. We encourage developers to have a look at our ",(0,s.jsx)(t.a,{href:"https://github.com/boostercloud/booster/tree/main/packages/framework-integration-tests",children:"Booster project repository"}),"."]}),"\n",(0,s.jsx)(t.p,{children:"Some integration tests highly depend on the provider chosen for the project, and the infrastructure is normally deployed in the cloud right before the tests run. Once tests are completed, the application is teared down."}),"\n",(0,s.jsx)(t.p,{children:"There are several types of integration tests in this package:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"Tests to ensure that different packages integrate as expected with each other."}),"\n",(0,s.jsx)(t.li,{children:"Tests to ensure that a Booster application behaves as expected when it is hit by a client (a GraphQL client)."}),"\n",(0,s.jsx)(t.li,{children:"Tests to ensure that the application behaves in the same way no matter what provider is selected."}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>r,a:()=>a});var s=n(7294);const o={},i=s.createContext(o);function a(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/0350e44c.56ec3e0c.js b/assets/js/0350e44c.56ec3e0c.js new file mode 100644 index 000000000..4e38b614d --- /dev/null +++ b/assets/js/0350e44c.56ec3e0c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[8946],{4380:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>r,toc:()=>l});var s=n(5893),o=n(1151);const i={},a="Testing",r={id:"going-deeper/testing",title:"Testing",description:"Booster applications are fully tested by default. This means that you can be sure that your application will work as expected. However, you can also write your own tests to check that your application behaves as you expect. In this section, we will leave some recommendations on how to test your Booster application.",source:"@site/docs/10_going-deeper/testing.md",sourceDirName:"10_going-deeper",slug:"/going-deeper/testing",permalink:"/going-deeper/testing",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/testing.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"sensor-health",permalink:"/going-deeper/health/sensor-health"},next:{title:"Migrations",permalink:"/going-deeper/data-migrations"}},c={},l=[{value:"Testing Booster applications",id:"testing-booster-applications",level:2},{value:"Testing with sinon-chai",id:"testing-with-sinon-chai",level:3},{value:"Recommended files",id:"recommended-files",level:3},{value:"Framework integration tests",id:"framework-integration-tests",level:2}];function d(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"testing",children:"Testing"}),"\n",(0,s.jsx)(t.p,{children:"Booster applications are fully tested by default. This means that you can be sure that your application will work as expected. However, you can also write your own tests to check that your application behaves as you expect. In this section, we will leave some recommendations on how to test your Booster application."}),"\n",(0,s.jsx)(t.h2,{id:"testing-booster-applications",children:"Testing Booster applications"}),"\n",(0,s.jsxs)(t.p,{children:["To properly test a Booster application, you should create a ",(0,s.jsx)(t.code,{children:"test"})," folder at the same level as the ",(0,s.jsx)(t.code,{children:"src"})," one. Apart from that, tests' names should have the ",(0,s.jsx)(t.code,{children:".test.ts"})," format."]}),"\n",(0,s.jsxs)(t.p,{children:["When a Booster application is generated, you will have a script in a ",(0,s.jsx)(t.code,{children:"package.json"})," like this:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:'"scripts": {\n "test": "nyc --extension .ts mocha --forbid-only \\"test/**/*.test.ts\\""\n}\n'})}),"\n",(0,s.jsxs)(t.p,{children:["The only thing that you should add to this line are the ",(0,s.jsx)(t.code,{children:"AWS_SDK_LOAD_CONFIG=true"})," and ",(0,s.jsx)(t.code,{children:"BOOSTER_ENV=test"})," environment variables, so the script will look like this:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:'"scripts": {\n "test": "AWS_SDK_LOAD_CONFIG=true BOOSTER_ENV=test nyc --extension .ts mocha --forbid-only \\"test/**/*.test.ts\\""\n}\n'})}),"\n",(0,s.jsxs)(t.h3,{id:"testing-with-sinon-chai",children:["Testing with ",(0,s.jsx)(t.code,{children:"sinon-chai"})]}),"\n",(0,s.jsxs)(t.p,{children:["The ",(0,s.jsx)(t.code,{children:"BoosterConfig"})," can be accessed through the ",(0,s.jsx)(t.code,{children:"Booster.config"})," on any part of a Booster application. To properly mock it for your objective, we really recommend to use sinon ",(0,s.jsx)(t.code,{children:"replace"})," method, after configuring your ",(0,s.jsx)(t.code,{children:"Booster.config"})," as desired."]}),"\n",(0,s.jsxs)(t.p,{children:['In the example below, we add 2 "empty" read-models, since we are iterating ',(0,s.jsx)(t.code,{children:"Booster.config.readModels"})," from a command handler:"]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// Test\nimport { replace } from 'sinon'\n\nconst config = new BoosterConfig('test')\nconfig.appName = 'testing-time'\nconfig.providerPackage = '@boostercloud/framework-provider-aws'\nconfig.readModels['WoW'] = {} as ReadModelMetadata\nconfig.readModels['Amazing'] = {} as ReadModelMetadata\nreplace(Booster, 'config', config)\n\nconst spyMyCall = spy(MyCommand, 'myCall')\nconst command = new MyCommand('1', true)\nconst register = new Register('request-id-1')\nconst registerSpy = spy(register, 'events')\nawait MyCommand.handle(command, register)\n\nexpect(spyMyCall).to.have.been.calledOnceWithExactly('WoW')\nexpect(spyMyCall).to.have.been.calledOnceWithExactly('Amazing')\nexpect(registerSpy).to.have.been.calledOnceWithExactly(new MyEvent('1', 'WoW'))\nexpect(registerSpy).to.have.been.calledOnceWithExactly(new MyEvent('1', 'Amazing'))\n"})}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// Example code\npublic static async handle(command: MyCommand, register: Register): Promise {\n const readModels = Booster.config.readModels\n for (const readModelName in readModels) {\n myCall(readModelName)\n register.events(new MyEvent(command.ID, readModelName))\n }\n}\n"})}),"\n",(0,s.jsx)(t.h3,{id:"recommended-files",children:"Recommended files"}),"\n",(0,s.jsx)(t.p,{children:"These are some files that might help you speed up your testing with Booster."}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"// /test/expect.ts\nimport * as chai from 'chai'\n\nchai.use(require('sinon-chai'))\nchai.use(require('chai-as-promised'))\n\nexport const expect = chai.expect\n"})}),"\n",(0,s.jsxs)(t.p,{children:["This ",(0,s.jsx)(t.code,{children:"expect"})," method will help you with some more additional methods like ",(0,s.jsx)(t.code,{children:"expect().to.have.been.calledOnceWithExactly()"})]}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-yaml",children:"# /.mocharc.yml\ndiff: true\nrequire: 'ts-node/register'\nextension:\n - ts\npackage: './package.json'\nrecursive: true\nreporter: 'spec'\ntimeout: 5000\nfull-trace: true\nbail: true\n"})}),"\n",(0,s.jsx)(t.h2,{id:"framework-integration-tests",children:"Framework integration tests"}),"\n",(0,s.jsxs)(t.p,{children:["Booster framework integration tests package is used to test the Booster project itself, but it is also an example of how a Booster application could be tested. We encourage developers to have a look at our ",(0,s.jsx)(t.a,{href:"https://github.com/boostercloud/booster/tree/main/packages/framework-integration-tests",children:"Booster project repository"}),"."]}),"\n",(0,s.jsx)(t.p,{children:"Some integration tests highly depend on the provider chosen for the project, and the infrastructure is normally deployed in the cloud right before the tests run. Once tests are completed, the application is teared down."}),"\n",(0,s.jsx)(t.p,{children:"There are several types of integration tests in this package:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"Tests to ensure that different packages integrate as expected with each other."}),"\n",(0,s.jsx)(t.li,{children:"Tests to ensure that a Booster application behaves as expected when it is hit by a client (a GraphQL client)."}),"\n",(0,s.jsx)(t.li,{children:"Tests to ensure that the application behaves in the same way no matter what provider is selected."}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>r,a:()=>a});var s=n(7294);const o={},i=s.createContext(o);function a(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/09ff0a1d.80683b86.js b/assets/js/09ff0a1d.80683b86.js deleted file mode 100644 index b202365c9..000000000 --- a/assets/js/09ff0a1d.80683b86.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[5263],{5298:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>s,toc:()=>c});var o=n(5893),a=n(1151);const i={description:"Learn how to migrate data in Booster"},r="Migrations",s={id:"going-deeper/data-migrations",title:"Migrations",description:"Learn how to migrate data in Booster",source:"@site/docs/10_going-deeper/data-migrations.md",sourceDirName:"10_going-deeper",slug:"/going-deeper/data-migrations",permalink:"/going-deeper/data-migrations",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/data-migrations.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{description:"Learn how to migrate data in Booster"},sidebar:"docs",previous:{title:"Testing",permalink:"/going-deeper/testing"},next:{title:"TouchEntities",permalink:"/going-deeper/touch-entities"}},d={},c=[{value:"Schema migrations",id:"schema-migrations",level:2},{value:"Data migrations",id:"data-migrations",level:2},{value:"Migrate to Booster version 1.19.0",id:"migrate-to-booster-version-1190",level:2},{value:"Migrate to Booster version 2.3.0",id:"migrate-to-booster-version-230",level:2}];function l(e){const t={code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"migrations",children:"Migrations"}),"\n",(0,o.jsx)(t.p,{children:"Migrations are a mechanism for updating or transforming the schemas of events and entities as your system evolves. This allows you to make changes to your data model without losing or corrupting existing data. There are two types of migration tools available in Booster: schema migrations and data migrations."}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:["\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"Schema migrations"})," are used to incrementally upgrade an event or entity from a past version to the next. They are applied lazily, meaning that they are performed on-the-fly whenever an event or entity is loaded. This allows you to make changes to your data model without having to manually update all existing artifacts, and makes it possible to apply changes without running lenghty migration processes."]}),"\n"]}),"\n",(0,o.jsxs)(t.li,{children:["\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"Data migrations"}),", on the other hand, behave as background processes that can actively change the existing values in the database for existing entities and read models. They are particularly useful for data migrations that cannot be performed automatically with schema migrations, or for updating existing read models after a schema change."]}),"\n"]}),"\n"]}),"\n",(0,o.jsx)(t.p,{children:"Together, schema and data migrations provide a flexible and powerful toolset for managing the evolution of your data model over time."}),"\n",(0,o.jsx)(t.h2,{id:"schema-migrations",children:"Schema migrations"}),"\n",(0,o.jsxs)(t.p,{children:["Booster handles classes annotated with ",(0,o.jsx)(t.code,{children:"@Migrates"})," as ",(0,o.jsx)(t.strong,{children:"schema migrations"}),". The migration functions defined inside will update an existing artifact (either an event or an entity) from a previous version to a newer one whenever that artifact is visited. Schema migrations are applied to events and entities lazyly, meaning that they are only applied when the event or entity is loaded. This ensures that the migration process is non-disruptive and does not affect the performance of your system. Schema migrations are also performed on-the-fly and the results are not written back to the database, as events are not revisited once the next snapshot is written in the database."]}),"\n",(0,o.jsxs)(t.p,{children:["For example, to upgrade a ",(0,o.jsx)(t.code,{children:"Product"})," entity from version 1 to version 2, you can write the following migration class:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"@Migrates(Product)\nexport class ProductMigration {\n @ToVersion(2, { fromSchema: ProductV1, toSchema: ProductV2 })\n public async changeNameFieldToDisplayName(old: ProductV1): Promise {\n return new ProductV2(\n old.id,\n old.sku,\n old.name,\n old.description,\n old.price,\n old.pictures,\n old.deleted\n )\n }\n}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["Notice that we've used the ",(0,o.jsx)(t.code,{children:"@ToVersion"})," decorator in the above example. This decorator not only tells Booster what schema upgrade this migration performs, it also informs it about the existence of a version, which is always an integer number. Booster will always use the latest version known to tag newly created artifacts, defaulting to 1 when no migrations are defined. This ensures that the schema of newly created events and entities is up-to-date and that they can be migrated as needed in the future."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"@ToVersion"})," decorator takes two parameters in addition to the version: ",(0,o.jsx)(t.code,{children:"fromSchema"})," and ",(0,o.jsx)(t.code,{children:"toSchema"}),". The fromSchema parameter is set to ",(0,o.jsx)(t.code,{children:"ProductV1"}),", while the ",(0,o.jsx)(t.code,{children:"toSchema"})," parameter is set to ",(0,o.jsx)(t.code,{children:"ProductV2"}),". This tells Booster that the migration is updating the ",(0,o.jsx)(t.code,{children:"Product"})," object from version 1 (as defined by the ",(0,o.jsx)(t.code,{children:"ProductV1"})," schema) to version 2 (as defined by the ",(0,o.jsx)(t.code,{children:"ProductV2"})," schema)."]}),"\n",(0,o.jsxs)(t.p,{children:["As Booster can easily read the structure of your classes, the schemas are described as plain classes that you can maintain as part of your code. The ",(0,o.jsx)(t.code,{children:"ProductV1"})," class represents the schema of the previous version of the ",(0,o.jsx)(t.code,{children:"Product"})," object with the properties and structure of the ",(0,o.jsx)(t.code,{children:"Product"})," object as it was defined in version 1. The ",(0,o.jsx)(t.code,{children:"ProductV2"})," class is an alias for the latest version of the Product object. You can use the ",(0,o.jsx)(t.code,{children:"Product"})," class here, there's no difference, but it's a good practice to create an alias for clarity."]}),"\n",(0,o.jsxs)(t.p,{children:["It's a good practice to define the schema classes (",(0,o.jsx)(t.code,{children:"ProductV1"})," and ",(0,o.jsx)(t.code,{children:"ProductV2"}),") as non-exported classes in the same migration file. This allows you to see the changes made between versions and helps to understand how the migration works:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"class ProductV1 {\n public constructor(\n public id: UUID,\n readonly sku: string,\n readonly name: string,\n readonly description: string,\n readonly price: Money,\n readonly pictures: Array,\n public deleted: boolean = false\n ) {}\n}\n\nclass ProductV2 extends Product {}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["When you want to upgrade your artifacts from V2 to V3, you can add a new function decorated with ",(0,o.jsx)(t.code,{children:"@ToVersion"})," to the same migrations class. You're free to structure the code the way you want, but we recommend keeping all migrations for the same artifact in the same migration class. For instance:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"@Migrates(Product)\nexport class ProductMigration {\n @ToVersion(2, { fromSchema: ProductV1, toSchema: ProductV2 })\n public async changeNameFieldToDisplayName(old: ProductV1): Promise {\n return new ProductV2(\n old.id,\n old.sku,\n old.name, // It's now called `displayName`\n old.description,\n old.price,\n old.pictures,\n old.deleted\n )\n }\n\n @ToVersion(3, { fromSchema: ProductV2, toSchema: ProductV3 })\n public async addNewField(old: ProductV2): Promise {\n return new ProductV3(\n old.id,\n old.sku,\n old.displayName,\n old.description,\n old.price,\n old.pictures,\n old.deleted,\n 42 // We set a default value to initialize this field\n )\n }\n}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["In this example, the ",(0,o.jsx)(t.code,{children:"changeNameFieldToDisplayName"})," function updates the ",(0,o.jsx)(t.code,{children:"Product"})," entity from version 1 to version 2 by renaming the ",(0,o.jsx)(t.code,{children:"name"})," field to ",(0,o.jsx)(t.code,{children:"displayName"}),". Then, ",(0,o.jsx)(t.code,{children:"addNewField"})," function updates the ",(0,o.jsx)(t.code,{children:"Product"})," entity from version 2 to version 3 by adding a new field called ",(0,o.jsx)(t.code,{children:"newField"})," to the entity's schema. Notice that at this point, your database could have snapshots set as v1, v2, or v3, so while it might be tempting to redefine the original migration to keep a single 1-to-3 migration, it's usually a good idea to keep the intermediate steps. This way Booster will be able to handle any scenario."]}),"\n",(0,o.jsx)(t.h2,{id:"data-migrations",children:"Data migrations"}),"\n",(0,o.jsx)(t.p,{children:"Data migrations can be seen as background processes that can actively update the values of existing entities and read models in the database. They can be useful to perform data migrations that cannot be handled with schema migrations, for example when you need to update the values exposed by the GraphQL API, or to initialize new read models that are projections of previously existing entities."}),"\n",(0,o.jsxs)(t.p,{children:["To create a data migration in Booster, you can use the ",(0,o.jsx)(t.code,{children:"@DataMigration"})," decorator on a class that implements a ",(0,o.jsx)(t.code,{children:"start"})," method. The ",(0,o.jsx)(t.code,{children:"@DataMigration"})," decorator takes an object with a single parameter, ",(0,o.jsx)(t.code,{children:"order"}),", which specifies the order in which the data migration should be run relative to other data migrations."]}),"\n",(0,o.jsxs)(t.p,{children:["Data migrations are not run automatically, you need to invoke the ",(0,o.jsx)(t.code,{children:"BoosterDataMigrations.run()"})," method from an event handler or a command. This will emit a ",(0,o.jsx)(t.code,{children:"BoosterDataMigrationStarted"})," event, which will make Booster check for any pending migrations and run them in the specified order. A common pattern to be able to run migrations on demand is to add a special command, with access limited to an administrator role which calls this function."]}),"\n",(0,o.jsxs)(t.p,{children:["Take into account that, depending on your cloud provider implementation, data migrations are executed in the context of a lambda or function app, so it's advisable to design these functions in a way that allow to re-run them in case of failures (i.e. lambda timeouts). In order to tell Booster that your migration has been applied successfully, at the end of each ",(0,o.jsx)(t.code,{children:"DataMigration.start"})," method, you must emit a ",(0,o.jsx)(t.code,{children:"BoosterDataMigrationFinished"})," event manually."]}),"\n",(0,o.jsxs)(t.p,{children:["Inside your ",(0,o.jsx)(t.code,{children:"@DataMigration"})," classes, you can use the ",(0,o.jsx)(t.code,{children:"BoosterDataMigrations.migrateEntity"})," method to update the data for a specific entity. This method takes the old entity name, the old entity ID, and the new entity data as arguments. It will also generate an internal ",(0,o.jsx)(t.code,{children:"BoosterEntityMigrated"})," event before performing the migration."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.strong,{children:"Note that Data migrations are only available in the Azure provider at the moment."})}),"\n",(0,o.jsxs)(t.p,{children:["Here is an example of how you might use the ",(0,o.jsx)(t.code,{children:"@DataMigration"})," decorator and the ",(0,o.jsx)(t.code,{children:"Booster.migrateEntity"})," method to update the quantity of the first item in a cart (",(0,o.jsxs)(t.strong,{children:["Notice that at the time of writing this document, the method ",(0,o.jsx)(t.code,{children:"Booster.entitiesIDs"})," used in the following example is only available in the Azure provider, so you may need to approach the migration differently in AWS."]}),"):"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"@DataMigration({\n order: 2,\n})\nexport class CartIdDataMigrateV2 {\n public constructor() {}\n\n\n public static async start(register: Register): Promise {\n const entitiesIdsResult = await Booster.entitiesIDs('Cart', 500, undefined)\n const paginatedEntityIdResults = entitiesIdsResult.items\n\n const carts = await Promise.all(\n paginatedEntityIdResults.map(async (entity) => await Booster.entity(Cart, entity.entityID))\n )\n return await Promise.all(\n carts.map(async (cart) => {\n cart.cartItems[0].quantity = 100\n const newCart = new Cart(cart.id, cart.cartItems, cart.shippingAddress, cart.checks)\n await BoosterDataMigrations.migrateEntity('Cart', validCart.id, newCart)\n return validCart.id\n })\n )\n\n register.events(new BoosterDataMigrationFinished('CartIdDataMigrateV2'))\n }\n}\n"})}),"\n",(0,o.jsx)(t.h1,{id:"migrate-from-previous-booster-versions",children:"Migrate from Previous Booster Versions"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"To migrate to new versions of Booster, check that you have the latest development dependencies required:"}),"\n"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-json",children:'"devDependencies": {\n "rimraf": "^5.0.0",\n "@typescript-eslint/eslint-plugin": "4.22.1",\n "@typescript-eslint/parser": "4.22.1",\n "eslint": "7.26.0",\n "eslint-config-prettier": "8.3.0",\n "eslint-plugin-prettier": "3.4.0",\n "mocha": "10.2.0",\n "@types/mocha": "10.0.1",\n "nyc": "15.1.0",\n "prettier": "2.3.0",\n "typescript": "4.5.4",\n "ts-node": "9.1.1",\n "@types/node": "15.0.2",\n "ts-patch": "2.0.2",\n "@boostercloud/metadata-booster": "0.30.2"\n },\n'})}),"\n",(0,o.jsx)(t.h2,{id:"migrate-to-booster-version-1190",children:"Migrate to Booster version 1.19.0"}),"\n",(0,o.jsxs)(t.p,{children:["Booster version 1.19.0 requires updating your index.ts file to export the ",(0,o.jsx)(t.code,{children:"boosterHealth"})," method. If you have an index.ts file created from a previous Booster version, update it accordingly. Example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"import { Booster } from '@boostercloud/framework-core'\nexport {\n Booster,\n boosterEventDispatcher,\n boosterServeGraphQL,\n boosterHealth,\n boosterNotifySubscribers,\n boosterTriggerScheduledCommand,\n boosterRocketDispatcher,\n} from '@boostercloud/framework-core'\n\nBooster.start(__dirname)\n\n"})}),"\n",(0,o.jsx)(t.h2,{id:"migrate-to-booster-version-230",children:"Migrate to Booster version 2.3.0"}),"\n",(0,o.jsxs)(t.p,{children:["Booster version 2.3.0 updates the url for the GraphQL API, sensors, etc. for the Azure Provider. New base url is ",(0,o.jsx)(t.code,{children:"http://[resourcegroupname]apis.eastus.cloudapp.azure.com"})]}),"\n",(0,o.jsx)(t.p,{children:"Also, Booster version 2.3.0 deprecated the Azure Api Management in favor of Azure Application Gateway. You don't need to do anything to migrate to the new Application Gateway."}),"\n",(0,o.jsxs)(t.p,{children:["Booster 2.3.0 provides an improved Rocket process to handle Rockets with more than one function. To use this new feature, you need to implement method ",(0,o.jsx)(t.code,{children:"mountCode"})," in your ",(0,o.jsx)(t.code,{children:"Rocket"})," class. Example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"const AzureWebhook = (params: WebhookParams): InfrastructureRocket => ({\n mountStack: Synth.mountStack.bind(Synth, params),\n mountCode: Functions.mountCode.bind(Synth, params),\n getFunctionAppName: Functions.getFunctionAppName.bind(Synth, params),\n})\n"})}),"\n",(0,o.jsx)(t.p,{children:"This method will return an Array of functions definitions, the function name, and the host.json file. Example:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"export interface FunctionAppFunctionsDefinition {\n functionAppName: string\n functionsDefinitions: Array>\n hostJsonPath?: string\n}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["Booster 2.3.0 allows you to set the app service plan used to deploy the main function app. Setting the ",(0,o.jsx)(t.code,{children:"BOOSTER_AZURE_SERVICE_PLAN_BASIC"})," environment variable to true will force the use of a basic service plan instead of the default consumption plan."]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var o=n(7294);const a={},i=o.createContext(a);function r(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/09ff0a1d.87ef4fe0.js b/assets/js/09ff0a1d.87ef4fe0.js new file mode 100644 index 000000000..3ee8f1942 --- /dev/null +++ b/assets/js/09ff0a1d.87ef4fe0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[5263],{5298:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>r,default:()=>h,frontMatter:()=>i,metadata:()=>s,toc:()=>c});var o=n(5893),a=n(1151);const i={description:"Learn how to migrate data in Booster"},r="Migrations",s={id:"going-deeper/data-migrations",title:"Migrations",description:"Learn how to migrate data in Booster",source:"@site/docs/10_going-deeper/data-migrations.md",sourceDirName:"10_going-deeper",slug:"/going-deeper/data-migrations",permalink:"/going-deeper/data-migrations",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/data-migrations.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{description:"Learn how to migrate data in Booster"},sidebar:"docs",previous:{title:"Testing",permalink:"/going-deeper/testing"},next:{title:"TouchEntities",permalink:"/going-deeper/touch-entities"}},d={},c=[{value:"Schema migrations",id:"schema-migrations",level:2},{value:"Data migrations",id:"data-migrations",level:2},{value:"Migrate to Booster version 1.19.0",id:"migrate-to-booster-version-1190",level:2},{value:"Migrate to Booster version 2.3.0",id:"migrate-to-booster-version-230",level:2}];function l(e){const t={code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"migrations",children:"Migrations"}),"\n",(0,o.jsx)(t.p,{children:"Migrations are a mechanism for updating or transforming the schemas of events and entities as your system evolves. This allows you to make changes to your data model without losing or corrupting existing data. There are two types of migration tools available in Booster: schema migrations and data migrations."}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:["\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"Schema migrations"})," are used to incrementally upgrade an event or entity from a past version to the next. They are applied lazily, meaning that they are performed on-the-fly whenever an event or entity is loaded. This allows you to make changes to your data model without having to manually update all existing artifacts, and makes it possible to apply changes without running lenghty migration processes."]}),"\n"]}),"\n",(0,o.jsxs)(t.li,{children:["\n",(0,o.jsxs)(t.p,{children:[(0,o.jsx)(t.strong,{children:"Data migrations"}),", on the other hand, behave as background processes that can actively change the existing values in the database for existing entities and read models. They are particularly useful for data migrations that cannot be performed automatically with schema migrations, or for updating existing read models after a schema change."]}),"\n"]}),"\n"]}),"\n",(0,o.jsx)(t.p,{children:"Together, schema and data migrations provide a flexible and powerful toolset for managing the evolution of your data model over time."}),"\n",(0,o.jsx)(t.h2,{id:"schema-migrations",children:"Schema migrations"}),"\n",(0,o.jsxs)(t.p,{children:["Booster handles classes annotated with ",(0,o.jsx)(t.code,{children:"@Migrates"})," as ",(0,o.jsx)(t.strong,{children:"schema migrations"}),". The migration functions defined inside will update an existing artifact (either an event or an entity) from a previous version to a newer one whenever that artifact is visited. Schema migrations are applied to events and entities lazyly, meaning that they are only applied when the event or entity is loaded. This ensures that the migration process is non-disruptive and does not affect the performance of your system. Schema migrations are also performed on-the-fly and the results are not written back to the database, as events are not revisited once the next snapshot is written in the database."]}),"\n",(0,o.jsxs)(t.p,{children:["For example, to upgrade a ",(0,o.jsx)(t.code,{children:"Product"})," entity from version 1 to version 2, you can write the following migration class:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"@Migrates(Product)\nexport class ProductMigration {\n @ToVersion(2, { fromSchema: ProductV1, toSchema: ProductV2 })\n public async changeNameFieldToDisplayName(old: ProductV1): Promise {\n return new ProductV2(\n old.id,\n old.sku,\n old.name,\n old.description,\n old.price,\n old.pictures,\n old.deleted\n )\n }\n}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["Notice that we've used the ",(0,o.jsx)(t.code,{children:"@ToVersion"})," decorator in the above example. This decorator not only tells Booster what schema upgrade this migration performs, it also informs it about the existence of a version, which is always an integer number. Booster will always use the latest version known to tag newly created artifacts, defaulting to 1 when no migrations are defined. This ensures that the schema of newly created events and entities is up-to-date and that they can be migrated as needed in the future."]}),"\n",(0,o.jsxs)(t.p,{children:["The ",(0,o.jsx)(t.code,{children:"@ToVersion"})," decorator takes two parameters in addition to the version: ",(0,o.jsx)(t.code,{children:"fromSchema"})," and ",(0,o.jsx)(t.code,{children:"toSchema"}),". The fromSchema parameter is set to ",(0,o.jsx)(t.code,{children:"ProductV1"}),", while the ",(0,o.jsx)(t.code,{children:"toSchema"})," parameter is set to ",(0,o.jsx)(t.code,{children:"ProductV2"}),". This tells Booster that the migration is updating the ",(0,o.jsx)(t.code,{children:"Product"})," object from version 1 (as defined by the ",(0,o.jsx)(t.code,{children:"ProductV1"})," schema) to version 2 (as defined by the ",(0,o.jsx)(t.code,{children:"ProductV2"})," schema)."]}),"\n",(0,o.jsxs)(t.p,{children:["As Booster can easily read the structure of your classes, the schemas are described as plain classes that you can maintain as part of your code. The ",(0,o.jsx)(t.code,{children:"ProductV1"})," class represents the schema of the previous version of the ",(0,o.jsx)(t.code,{children:"Product"})," object with the properties and structure of the ",(0,o.jsx)(t.code,{children:"Product"})," object as it was defined in version 1. The ",(0,o.jsx)(t.code,{children:"ProductV2"})," class is an alias for the latest version of the Product object. You can use the ",(0,o.jsx)(t.code,{children:"Product"})," class here, there's no difference, but it's a good practice to create an alias for clarity."]}),"\n",(0,o.jsxs)(t.p,{children:["It's a good practice to define the schema classes (",(0,o.jsx)(t.code,{children:"ProductV1"})," and ",(0,o.jsx)(t.code,{children:"ProductV2"}),") as non-exported classes in the same migration file. This allows you to see the changes made between versions and helps to understand how the migration works:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"class ProductV1 {\n public constructor(\n public id: UUID,\n readonly sku: string,\n readonly name: string,\n readonly description: string,\n readonly price: Money,\n readonly pictures: Array,\n public deleted: boolean = false\n ) {}\n}\n\nclass ProductV2 extends Product {}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["When you want to upgrade your artifacts from V2 to V3, you can add a new function decorated with ",(0,o.jsx)(t.code,{children:"@ToVersion"})," to the same migrations class. You're free to structure the code the way you want, but we recommend keeping all migrations for the same artifact in the same migration class. For instance:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"@Migrates(Product)\nexport class ProductMigration {\n @ToVersion(2, { fromSchema: ProductV1, toSchema: ProductV2 })\n public async changeNameFieldToDisplayName(old: ProductV1): Promise {\n return new ProductV2(\n old.id,\n old.sku,\n old.name, // It's now called `displayName`\n old.description,\n old.price,\n old.pictures,\n old.deleted\n )\n }\n\n @ToVersion(3, { fromSchema: ProductV2, toSchema: ProductV3 })\n public async addNewField(old: ProductV2): Promise {\n return new ProductV3(\n old.id,\n old.sku,\n old.displayName,\n old.description,\n old.price,\n old.pictures,\n old.deleted,\n 42 // We set a default value to initialize this field\n )\n }\n}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["In this example, the ",(0,o.jsx)(t.code,{children:"changeNameFieldToDisplayName"})," function updates the ",(0,o.jsx)(t.code,{children:"Product"})," entity from version 1 to version 2 by renaming the ",(0,o.jsx)(t.code,{children:"name"})," field to ",(0,o.jsx)(t.code,{children:"displayName"}),". Then, ",(0,o.jsx)(t.code,{children:"addNewField"})," function updates the ",(0,o.jsx)(t.code,{children:"Product"})," entity from version 2 to version 3 by adding a new field called ",(0,o.jsx)(t.code,{children:"newField"})," to the entity's schema. Notice that at this point, your database could have snapshots set as v1, v2, or v3, so while it might be tempting to redefine the original migration to keep a single 1-to-3 migration, it's usually a good idea to keep the intermediate steps. This way Booster will be able to handle any scenario."]}),"\n",(0,o.jsx)(t.h2,{id:"data-migrations",children:"Data migrations"}),"\n",(0,o.jsx)(t.p,{children:"Data migrations can be seen as background processes that can actively update the values of existing entities and read models in the database. They can be useful to perform data migrations that cannot be handled with schema migrations, for example when you need to update the values exposed by the GraphQL API, or to initialize new read models that are projections of previously existing entities."}),"\n",(0,o.jsxs)(t.p,{children:["To create a data migration in Booster, you can use the ",(0,o.jsx)(t.code,{children:"@DataMigration"})," decorator on a class that implements a ",(0,o.jsx)(t.code,{children:"start"})," method. The ",(0,o.jsx)(t.code,{children:"@DataMigration"})," decorator takes an object with a single parameter, ",(0,o.jsx)(t.code,{children:"order"}),", which specifies the order in which the data migration should be run relative to other data migrations."]}),"\n",(0,o.jsxs)(t.p,{children:["Data migrations are not run automatically, you need to invoke the ",(0,o.jsx)(t.code,{children:"BoosterDataMigrations.run()"})," method from an event handler or a command. This will emit a ",(0,o.jsx)(t.code,{children:"BoosterDataMigrationStarted"})," event, which will make Booster check for any pending migrations and run them in the specified order. A common pattern to be able to run migrations on demand is to add a special command, with access limited to an administrator role which calls this function."]}),"\n",(0,o.jsxs)(t.p,{children:["Take into account that, depending on your cloud provider implementation, data migrations are executed in the context of a lambda or function app, so it's advisable to design these functions in a way that allow to re-run them in case of failures (i.e. lambda timeouts). In order to tell Booster that your migration has been applied successfully, at the end of each ",(0,o.jsx)(t.code,{children:"DataMigration.start"})," method, you must emit a ",(0,o.jsx)(t.code,{children:"BoosterDataMigrationFinished"})," event manually."]}),"\n",(0,o.jsxs)(t.p,{children:["Inside your ",(0,o.jsx)(t.code,{children:"@DataMigration"})," classes, you can use the ",(0,o.jsx)(t.code,{children:"BoosterDataMigrations.migrateEntity"})," method to update the data for a specific entity. This method takes the old entity name, the old entity ID, and the new entity data as arguments. It will also generate an internal ",(0,o.jsx)(t.code,{children:"BoosterEntityMigrated"})," event before performing the migration."]}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.strong,{children:"Note that Data migrations are only available in the Azure provider at the moment."})}),"\n",(0,o.jsxs)(t.p,{children:["Here is an example of how you might use the ",(0,o.jsx)(t.code,{children:"@DataMigration"})," decorator and the ",(0,o.jsx)(t.code,{children:"Booster.migrateEntity"})," method to update the quantity of the first item in a cart (",(0,o.jsxs)(t.strong,{children:["Notice that at the time of writing this document, the method ",(0,o.jsx)(t.code,{children:"Booster.entitiesIDs"})," used in the following example is only available in the Azure provider, so you may need to approach the migration differently in AWS."]}),"):"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"@DataMigration({\n order: 2,\n})\nexport class CartIdDataMigrateV2 {\n public constructor() {}\n\n\n public static async start(register: Register): Promise {\n const entitiesIdsResult = await Booster.entitiesIDs('Cart', 500, undefined)\n const paginatedEntityIdResults = entitiesIdsResult.items\n\n const carts = await Promise.all(\n paginatedEntityIdResults.map(async (entity) => await Booster.entity(Cart, entity.entityID))\n )\n return await Promise.all(\n carts.map(async (cart) => {\n cart.cartItems[0].quantity = 100\n const newCart = new Cart(cart.id, cart.cartItems, cart.shippingAddress, cart.checks)\n await BoosterDataMigrations.migrateEntity('Cart', validCart.id, newCart)\n return validCart.id\n })\n )\n\n register.events(new BoosterDataMigrationFinished('CartIdDataMigrateV2'))\n }\n}\n"})}),"\n",(0,o.jsx)(t.h1,{id:"migrate-from-previous-booster-versions",children:"Migrate from Previous Booster Versions"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"To migrate to new versions of Booster, check that you have the latest development dependencies required:"}),"\n"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-json",children:'"devDependencies": {\n "rimraf": "^5.0.0",\n "@typescript-eslint/eslint-plugin": "4.22.1",\n "@typescript-eslint/parser": "4.22.1",\n "eslint": "7.26.0",\n "eslint-config-prettier": "8.3.0",\n "eslint-plugin-prettier": "3.4.0",\n "mocha": "10.2.0",\n "@types/mocha": "10.0.1",\n "nyc": "15.1.0",\n "prettier": "2.3.0",\n "typescript": "4.5.4",\n "ts-node": "9.1.1",\n "@types/node": "15.0.2",\n "ts-patch": "2.0.2",\n "@boostercloud/metadata-booster": "0.30.2"\n },\n'})}),"\n",(0,o.jsx)(t.h2,{id:"migrate-to-booster-version-1190",children:"Migrate to Booster version 1.19.0"}),"\n",(0,o.jsxs)(t.p,{children:["Booster version 1.19.0 requires updating your index.ts file to export the ",(0,o.jsx)(t.code,{children:"boosterHealth"})," method. If you have an index.ts file created from a previous Booster version, update it accordingly. Example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"import { Booster } from '@boostercloud/framework-core'\nexport {\n Booster,\n boosterEventDispatcher,\n boosterServeGraphQL,\n boosterHealth,\n boosterNotifySubscribers,\n boosterTriggerScheduledCommand,\n boosterRocketDispatcher,\n} from '@boostercloud/framework-core'\n\nBooster.start(__dirname)\n\n"})}),"\n",(0,o.jsx)(t.h2,{id:"migrate-to-booster-version-230",children:"Migrate to Booster version 2.3.0"}),"\n",(0,o.jsxs)(t.p,{children:["Booster version 2.3.0 updates the url for the GraphQL API, sensors, etc. for the Azure Provider. New base url is ",(0,o.jsx)(t.code,{children:"http://[resourcegroupname]apis.eastus.cloudapp.azure.com"})]}),"\n",(0,o.jsx)(t.p,{children:"Also, Booster version 2.3.0 deprecated the Azure Api Management in favor of Azure Application Gateway. You don't need to do anything to migrate to the new Application Gateway."}),"\n",(0,o.jsxs)(t.p,{children:["Booster 2.3.0 provides an improved Rocket process to handle Rockets with more than one function. To use this new feature, you need to implement method ",(0,o.jsx)(t.code,{children:"mountCode"})," in your ",(0,o.jsx)(t.code,{children:"Rocket"})," class. Example:"]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"const AzureWebhook = (params: WebhookParams): InfrastructureRocket => ({\n mountStack: Synth.mountStack.bind(Synth, params),\n mountCode: Functions.mountCode.bind(Synth, params),\n getFunctionAppName: Functions.getFunctionAppName.bind(Synth, params),\n})\n"})}),"\n",(0,o.jsx)(t.p,{children:"This method will return an Array of functions definitions, the function name, and the host.json file. Example:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.code,{className:"language-typescript",children:"export interface FunctionAppFunctionsDefinition {\n functionAppName: string\n functionsDefinitions: Array>\n hostJsonPath?: string\n}\n"})}),"\n",(0,o.jsxs)(t.p,{children:["Booster 2.3.0 allows you to set the app service plan used to deploy the main function app. Setting the ",(0,o.jsx)(t.code,{children:"BOOSTER_AZURE_SERVICE_PLAN_BASIC"})," environment variable to true will force the use of a basic service plan instead of the default consumption plan."]})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>r});var o=n(7294);const a={},i=o.createContext(a);function r(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/10057e71.78e107cf.js b/assets/js/10057e71.78e107cf.js new file mode 100644 index 000000000..c5d0f0a6e --- /dev/null +++ b/assets/js/10057e71.78e107cf.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4274],{3897:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>a,contentTitle:()=>i,default:()=>j,frontMatter:()=>s,metadata:()=>c,toc:()=>l});var r=n(5893),d=n(1151),o=n(5163);const s={},i="Booster CLI",c={id:"booster-cli",title:"Booster CLI",description:"Booster CLI is a command line interface that helps you to create, develop, and deploy your Booster applications. It is built with Node.js and published to NPM through the package @boostercloud/cli . You can install it using any compatible package manager. If you want to contribute to the project, you will also need to clone the GitHub repository and compile the source code.",source:"@site/docs/05_booster-cli.mdx",sourceDirName:".",slug:"/booster-cli",permalink:"/booster-cli",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/05_booster-cli.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:5,frontMatter:{},sidebar:"docs",previous:{title:"GraphQL API",permalink:"/graphql"},next:{title:"Going deeper with Booster",permalink:"/category/going-deeper-with-booster"}},a={},l=[{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"Command Overview",id:"command-overview",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,d.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"booster-cli",children:"Booster CLI"}),"\n",(0,r.jsxs)(t.p,{children:["Booster CLI is a command line interface that helps you to create, develop, and deploy your Booster applications. It is built with Node.js and published to NPM through the package ",(0,r.jsx)(t.code,{children:"@boostercloud/cli"})," . You can install it using any compatible package manager. If you want to contribute to the project, you will also need to clone the GitHub repository and compile the source code."]}),"\n",(0,r.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsxs)(t.p,{children:["The preferred way to install the Booster CLI is through NPM. You can install it following the instructions in the ",(0,r.jsx)(t.a,{href:"https://nodejs.org/en/download/",children:"Node.js website"}),"."]}),"\n",(0,r.jsx)(t.p,{children:"Once you have NPM installed, you can install the Booster CLI by running this command:"}),"\n",(0,r.jsx)(o.Z,{children:(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm install -g @boostercloud/cli\n"})})}),"\n",(0,r.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(t.p,{children:["Once the installation is finished, you will have the ",(0,r.jsx)(t.code,{children:"boost"})," command available in your terminal. You can run it to see the help message."]}),"\n",(0,r.jsx)(t.admonition,{type:"tip",children:(0,r.jsxs)(t.p,{children:["You can also run ",(0,r.jsx)(t.code,{children:"boost --help"})," to get the same output."]})}),"\n",(0,r.jsx)(t.h2,{id:"command-overview",children:"Command Overview"}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Command"}),(0,r.jsx)(t.th,{children:"Description"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"#new",children:(0,r.jsx)(t.code,{children:"new:project"})})}),(0,r.jsx)(t.td,{children:"Creates a new Booster project in a new directory"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/command#creating-a-command",children:(0,r.jsx)(t.code,{children:"new:command"})})}),(0,r.jsx)(t.td,{children:"Creates a new command in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/entity#creating-an-entity",children:(0,r.jsx)(t.code,{children:"new:entity"})})}),(0,r.jsx)(t.td,{children:"Creates a new entity in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/event#creating-an-event",children:(0,r.jsx)(t.code,{children:"new:event"})})}),(0,r.jsx)(t.td,{children:"Creates a new event in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/event-handler#creating-an-event-handler",children:(0,r.jsx)(t.code,{children:"new:event-handler"})})}),(0,r.jsx)(t.td,{children:"Creates a new event handler in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/read-model#creating-a-read-model",children:(0,r.jsx)(t.code,{children:"new:read-model"})})}),(0,r.jsx)(t.td,{children:"Creates a new read model in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/features/schedule-actions#creating-a-scheduled-command",children:(0,r.jsx)(t.code,{children:"new:scheduled-command"})})}),(0,r.jsx)(t.td,{children:"Creates a new scheduled command in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{}),(0,r.jsx)(t.td,{})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/getting-started/coding#6-deployment",children:(0,r.jsx)(t.code,{children:"start -e "})})}),(0,r.jsx)(t.td,{children:"Starts the project in development mode"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/getting-started/coding#6-deployment",children:(0,r.jsx)(t.code,{children:"build"})})}),(0,r.jsx)(t.td,{children:"Builds the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/getting-started/coding#6-deployment",children:(0,r.jsx)(t.code,{children:"deploy -e "})})}),(0,r.jsx)(t.td,{children:"Deploys the project to the cloud"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"nuke"})}),(0,r.jsx)(t.td,{children:"Deletes all the resources created by the deploy command"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{}),(0,r.jsx)(t.td,{})]})]})]})]})}function j(e={}){const{wrapper:t}={...(0,d.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},5163:(e,t,n)=>{n.d(t,{Z:()=>o});n(7294);const r={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var d=n(5893);function o(e){let{children:t}=e;return(0,d.jsxs)("div",{className:r.terminalWindow,children:[(0,d.jsx)("div",{className:r.terminalWindowHeader,children:(0,d.jsxs)("div",{className:r.buttons,children:[(0,d.jsx)("span",{className:r.dot,style:{background:"#f25f58"}}),(0,d.jsx)("span",{className:r.dot,style:{background:"#fbbe3c"}}),(0,d.jsx)("span",{className:r.dot,style:{background:"#58cb42"}})]})}),(0,d.jsx)("div",{className:r.terminalWindowBody,children:t})]})}},1151:(e,t,n)=>{n.d(t,{Z:()=>i,a:()=>s});var r=n(7294);const d={},o=r.createContext(d);function s(e){const t=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:s(e.components),r.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/10057e71.d684e5ce.js b/assets/js/10057e71.d684e5ce.js deleted file mode 100644 index 61a361a2c..000000000 --- a/assets/js/10057e71.d684e5ce.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4274],{3897:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>j,frontMatter:()=>s,metadata:()=>a,toc:()=>l});var r=n(5893),d=n(1151),o=n(5163);const s={},i="Booster CLI",a={id:"booster-cli",title:"Booster CLI",description:"Booster CLI is a command line interface that helps you to create, develop, and deploy your Booster applications. It is built with Node.js and published to NPM through the package @boostercloud/cli . You can install it using any compatible package manager. If you want to contribute to the project, you will also need to clone the GitHub repository and compile the source code.",source:"@site/docs/05_booster-cli.mdx",sourceDirName:".",slug:"/booster-cli",permalink:"/booster-cli",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/05_booster-cli.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:5,frontMatter:{},sidebar:"docs",previous:{title:"GraphQL API",permalink:"/graphql"},next:{title:"Going deeper with Booster",permalink:"/category/going-deeper-with-booster"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"Command Overview",id:"command-overview",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,d.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"booster-cli",children:"Booster CLI"}),"\n",(0,r.jsxs)(t.p,{children:["Booster CLI is a command line interface that helps you to create, develop, and deploy your Booster applications. It is built with Node.js and published to NPM through the package ",(0,r.jsx)(t.code,{children:"@boostercloud/cli"})," . You can install it using any compatible package manager. If you want to contribute to the project, you will also need to clone the GitHub repository and compile the source code."]}),"\n",(0,r.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsxs)(t.p,{children:["The preferred way to install the Booster CLI is through NPM. You can install it following the instructions in the ",(0,r.jsx)(t.a,{href:"https://nodejs.org/en/download/",children:"Node.js website"}),"."]}),"\n",(0,r.jsx)(t.p,{children:"Once you have NPM installed, you can install the Booster CLI by running this command:"}),"\n",(0,r.jsx)(o.Z,{children:(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm install -g @boostercloud/cli\n"})})}),"\n",(0,r.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(t.p,{children:["Once the installation is finished, you will have the ",(0,r.jsx)(t.code,{children:"boost"})," command available in your terminal. You can run it to see the help message."]}),"\n",(0,r.jsx)(t.admonition,{type:"tip",children:(0,r.jsxs)(t.p,{children:["You can also run ",(0,r.jsx)(t.code,{children:"boost --help"})," to get the same output."]})}),"\n",(0,r.jsx)(t.h2,{id:"command-overview",children:"Command Overview"}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Command"}),(0,r.jsx)(t.th,{children:"Description"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"#new",children:(0,r.jsx)(t.code,{children:"new:project"})})}),(0,r.jsx)(t.td,{children:"Creates a new Booster project in a new directory"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/command#creating-a-command",children:(0,r.jsx)(t.code,{children:"new:command"})})}),(0,r.jsx)(t.td,{children:"Creates a new command in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/entity#creating-an-entity",children:(0,r.jsx)(t.code,{children:"new:entity"})})}),(0,r.jsx)(t.td,{children:"Creates a new entity in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/event#creating-an-event",children:(0,r.jsx)(t.code,{children:"new:event"})})}),(0,r.jsx)(t.td,{children:"Creates a new event in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/event-handler#creating-an-event-handler",children:(0,r.jsx)(t.code,{children:"new:event-handler"})})}),(0,r.jsx)(t.td,{children:"Creates a new event handler in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/architecture/read-model#creating-a-read-model",children:(0,r.jsx)(t.code,{children:"new:read-model"})})}),(0,r.jsx)(t.td,{children:"Creates a new read model in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/features/schedule-actions#creating-a-scheduled-command",children:(0,r.jsx)(t.code,{children:"new:scheduled-command"})})}),(0,r.jsx)(t.td,{children:"Creates a new scheduled command in the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{}),(0,r.jsx)(t.td,{})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/getting-started/coding#6-deployment",children:(0,r.jsx)(t.code,{children:"start -e "})})}),(0,r.jsx)(t.td,{children:"Starts the project in development mode"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/getting-started/coding#6-deployment",children:(0,r.jsx)(t.code,{children:"build"})})}),(0,r.jsx)(t.td,{children:"Builds the project"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.a,{href:"/getting-started/coding#6-deployment",children:(0,r.jsx)(t.code,{children:"deploy -e "})})}),(0,r.jsx)(t.td,{children:"Deploys the project to the cloud"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"nuke"})}),(0,r.jsx)(t.td,{children:"Deletes all the resources created by the deploy command"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{}),(0,r.jsx)(t.td,{})]})]})]})]})}function j(e={}){const{wrapper:t}={...(0,d.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},5163:(e,t,n)=>{n.d(t,{Z:()=>o});n(7294);const r={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var d=n(5893);function o(e){let{children:t}=e;return(0,d.jsxs)("div",{className:r.terminalWindow,children:[(0,d.jsx)("div",{className:r.terminalWindowHeader,children:(0,d.jsxs)("div",{className:r.buttons,children:[(0,d.jsx)("span",{className:r.dot,style:{background:"#f25f58"}}),(0,d.jsx)("span",{className:r.dot,style:{background:"#fbbe3c"}}),(0,d.jsx)("span",{className:r.dot,style:{background:"#58cb42"}})]})}),(0,d.jsx)("div",{className:r.terminalWindowBody,children:t})]})}},1151:(e,t,n)=>{n.d(t,{Z:()=>i,a:()=>s});var r=n(7294);const d={},o=r.createContext(d);function s(e){const t=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:s(e.components),r.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/192d5973.62d324d1.js b/assets/js/192d5973.62d324d1.js new file mode 100644 index 000000000..2ace30912 --- /dev/null +++ b/assets/js/192d5973.62d324d1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1300],{9210:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>a,contentTitle:()=>i,default:()=>p,frontMatter:()=>n,metadata:()=>c,toc:()=>d});var s=o(5893),r=o(1151);const n={},i="Static Sites Rocket",c={id:"going-deeper/rockets/rocket-static-sites",title:"Static Sites Rocket",description:"This package is a configurable Booster rocket to add static site deployment to your Booster applications. It uploads your root.",source:"@site/docs/10_going-deeper/rockets/rocket-static-sites.md",sourceDirName:"10_going-deeper/rockets",slug:"/going-deeper/rockets/rocket-static-sites",permalink:"/going-deeper/rockets/rocket-static-sites",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/rockets/rocket-static-sites.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Backup Booster Rocket",permalink:"/going-deeper/rockets/rocket-backup-booster"},next:{title:"Webhook Rocket",permalink:"/going-deeper/rockets/rocket-webhook"}},a={},d=[{value:"Usage",id:"usage",level:2}];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"static-sites-rocket",children:"Static Sites Rocket"}),"\n",(0,s.jsx)(t.p,{children:"This package is a configurable Booster rocket to add static site deployment to your Booster applications. It uploads your root."}),"\n",(0,s.jsx)(t.admonition,{type:"info",children:(0,s.jsx)(t.p,{children:(0,s.jsx)(t.a,{href:"https://github.com/boostercloud/rocket-static-sites-aws-infrastructure",children:"GitHub Repo"})})}),"\n",(0,s.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsx)(t.p,{children:"Install this package as a dev dependency in your Booster project (It's a dev dependency because it's only used during deployment, but we don't want this code to be uploaded to the project lambdas)"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:"npm install --save-dev @boostercloud/rocket-static-sites-aws-infrastructure\n"})}),"\n",(0,s.jsx)(t.p,{children:"In your Booster config file, pass a RocketDescriptor in the config.rockets array to configuring the static site rocket:"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-typescript",children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\n\nBooster.configure('development', (config: BoosterConfig): void => {\n config.appName = 'my-store'\n config.rockets = [\n {\n packageName: '@boostercloud/rocket-static-sites-aws-infrastructure', \n parameters: {\n bucketName: 'test-bucket-name', // Required\n rootPath: './frontend/dist', // Defaults to ./public\n indexFile: 'main.html', // File to render when users access the CLoudFormation URL. Defaults to index.html\n errorFile: 'error.html', // File to render when there's an error. Defaults to 404.html\n }\n },\n ]\n})\n"})})]})}function p(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>c,a:()=>i});var s=o(7294);const r={},n=s.createContext(r);function i(e){const t=s.useContext(n);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),s.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/192d5973.cc901893.js b/assets/js/192d5973.cc901893.js deleted file mode 100644 index 7a166ac62..000000000 --- a/assets/js/192d5973.cc901893.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1300],{9210:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>a,contentTitle:()=>i,default:()=>p,frontMatter:()=>n,metadata:()=>c,toc:()=>d});var r=o(5893),s=o(1151);const n={},i="Static Sites Rocket",c={id:"going-deeper/rockets/rocket-static-sites",title:"Static Sites Rocket",description:"This package is a configurable Booster rocket to add static site deployment to your Booster applications. It uploads your root.",source:"@site/docs/10_going-deeper/rockets/rocket-static-sites.md",sourceDirName:"10_going-deeper/rockets",slug:"/going-deeper/rockets/rocket-static-sites",permalink:"/going-deeper/rockets/rocket-static-sites",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/rockets/rocket-static-sites.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Backup Booster Rocket",permalink:"/going-deeper/rockets/rocket-backup-booster"},next:{title:"Webhook Rocket",permalink:"/going-deeper/rockets/rocket-webhook"}},a={},d=[{value:"Usage",id:"usage",level:2}];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,s.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"static-sites-rocket",children:"Static Sites Rocket"}),"\n",(0,r.jsx)(t.p,{children:"This package is a configurable Booster rocket to add static site deployment to your Booster applications. It uploads your root."}),"\n",(0,r.jsx)(t.admonition,{type:"info",children:(0,r.jsx)(t.p,{children:(0,r.jsx)(t.a,{href:"https://github.com/boostercloud/rocket-static-sites-aws-infrastructure",children:"GitHub Repo"})})}),"\n",(0,r.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsx)(t.p,{children:"Install this package as a dev dependency in your Booster project (It's a dev dependency because it's only used during deployment, but we don't want this code to be uploaded to the project lambdas)"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:"npm install --save-dev @boostercloud/rocket-static-sites-aws-infrastructure\n"})}),"\n",(0,r.jsx)(t.p,{children:"In your Booster config file, pass a RocketDescriptor in the config.rockets array to configuring the static site rocket:"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-typescript",children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\n\nBooster.configure('development', (config: BoosterConfig): void => {\n config.appName = 'my-store'\n config.rockets = [\n {\n packageName: '@boostercloud/rocket-static-sites-aws-infrastructure', \n parameters: {\n bucketName: 'test-bucket-name', // Required\n rootPath: './frontend/dist', // Defaults to ./public\n indexFile: 'main.html', // File to render when users access the CLoudFormation URL. Defaults to index.html\n errorFile: 'error.html', // File to render when there's an error. Defaults to 404.html\n }\n },\n ]\n})\n"})})]})}function p(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>c,a:()=>i});var r=o(7294);const s={},n=r.createContext(s);function i(e){const t=r.useContext(n);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),r.createElement(n.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1b08e8f8.d3ed37d1.js b/assets/js/1b08e8f8.d3ed37d1.js deleted file mode 100644 index 91845c5bd..000000000 --- a/assets/js/1b08e8f8.d3ed37d1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1588],{6637:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>u});var o=r(5893),t=r(1151),s=r(5162),i=r(4866);const a={},l="File Uploads Rocket",c={id:"going-deeper/rockets/rocket-file-uploads",title:"File Uploads Rocket",description:"This package is a configurable rocket to add a storage API to your Booster applications.",source:"@site/docs/10_going-deeper/rockets/rocket-file-uploads.md",sourceDirName:"10_going-deeper/rockets",slug:"/going-deeper/rockets/rocket-file-uploads",permalink:"/going-deeper/rockets/rocket-file-uploads",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/rockets/rocket-file-uploads.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Extending Booster with Rockets!",permalink:"/going-deeper/rockets"},next:{title:"Backup Booster Rocket",permalink:"/going-deeper/rockets/rocket-backup-booster"}},d={},u=[{value:"Supported Providers",id:"supported-providers",level:2},{value:"Overview",id:"overview",level:2},{value:"Usage",id:"usage",level:2},{value:"Rocket Methods Usage",id:"rocket-methods-usage",level:2},{value:"Azure Roles",id:"azure-roles",level:2},{value:"Rocket Methods Usage",id:"rocket-methods-usage-1",level:2},{value:"Rocket Methods Usage",id:"rocket-methods-usage-2",level:2},{value:"Security",id:"security",level:2},{value:"Events",id:"events",level:2},{value:"TODOs",id:"todos",level:2}];function m(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.a)(),...e.components},{Details:r}=n;return r||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"file-uploads-rocket",children:"File Uploads Rocket"}),"\n",(0,o.jsx)(n.p,{children:"This package is a configurable rocket to add a storage API to your Booster applications."}),"\n",(0,o.jsx)(n.admonition,{type:"info",children:(0,o.jsx)(n.p,{children:(0,o.jsx)(n.a,{href:"https://github.com/boostercloud/rocket-file-uploads",children:"GitHub Repo"})})}),"\n",(0,o.jsx)(n.h2,{id:"supported-providers",children:"Supported Providers"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"Azure Provider"}),"\n",(0,o.jsx)(n.li,{children:"AWS Provider"}),"\n",(0,o.jsx)(n.li,{children:"Local Provider"}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,o.jsx)(n.p,{children:"This rocket provides some methods to access files stores in your cloud provider:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"presignedPut"}),": Returns a presigned put url and the necessary form params. With this url files can be uploaded directly to your provider."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"presignedGet"}),": Returns a presigned get url to download a file. With this url files can be downloaded directly from your provider."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"list"}),": Returns a list of files stored in the provider."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"deleteFile"}),": Removes a file from a directory (only supported in AWS at the moment)."]}),"\n"]}),"\n",(0,o.jsx)(n.p,{children:"These methods may be used from a Command in your project secured via JWT Token.\nThis rocket also provides a Booster Event each time a file is uploaded."}),"\n",(0,o.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,o.jsxs)(i.Z,{groupId:"providers-usage",children:[(0,o.jsxs)(s.Z,{value:"azure-provider",label:"Azure Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"Install needed dependency packages:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save @boostercloud/rocket-file-uploads-core @boostercloud/rocket-file-uploads-types\nnpm install --save @boostercloud/rocket-file-uploads-azure\n"})}),(0,o.jsx)(n.p,{children:"Also, you will need a devDependency in your project:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save-dev @boostercloud/rocket-file-uploads-azure-infrastructure\n"})}),(0,o.jsx)(n.p,{children:"In your Booster config file, configure your BoosterRocketFiles:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/config/config.ts"',children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\nimport { BoosterRocketFiles } from '@boostercloud/rocket-file-uploads-core'\nimport { RocketFilesUserConfiguration } from '@boostercloud/rocket-file-uploads-types'\n\nconst rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {\n storageName: 'STORAGE_NAME',\n containerName: 'CONTAINER_NAME',\n directories: ['DIRECTORY_1', 'DIRECTORY_2'],\n}\n\nconst rocketFilesConfigurationCms: RocketFilesUserConfiguration = {\n storageName: 'cmsst',\n containerName: 'rocketfiles',\n directories: ['cms1', 'cms2'],\n}\n\nBooster.configure('production', (config: BoosterConfig): void => {\n config.appName = 'TEST_APP_NAME'\n config.providerPackage = '@boostercloud/framework-provider-azure'\n config.rockets = [\n new BoosterRocketFiles(config, [rocketFilesConfigurationDefault, rocketFilesConfigurationCms]).rocketForAzure(),\n ]\n})\n\n"})}),(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsx)(n.p,{children:"Available parameters are:"}),(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"storageName"}),": Name of the storage repository."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"containerName"}),": Directories container."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"directories"}),": A list of folders where the files will be stored."]}),"\n"]}),(0,o.jsx)(n.hr,{}),(0,o.jsx)(n.p,{children:"The structure created will be:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 storageName\n\u2502 \u251c\u2500\u2500 containerName\n\u2502 \u2502 \u251c\u2500\u2500 directory\n"})}),(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"NOTE:"})," Azure Provider will use ",(0,o.jsx)(n.code,{children:"storageName"})," as the Storage Account Name."]})]}),(0,o.jsx)(n.h2,{id:"rocket-methods-usage",children:"Rocket Methods Usage"}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Put"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedPut"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to upload on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-put.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadPut {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadPut, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedPut(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadPut(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Azure Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadPut": "https://clientst.blob.core.windows.net/rocketfiles/client1/myfile.txt?"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Get"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedGet"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to get on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-get.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadGet {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadGet, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedGet(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadGet(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Azure Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadGet": "https://clientst.blob.core.windows.net/rocketfiles/folder01%2Fmyfile.txt?"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"List"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"list"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory you want to get the info and return the formatted results."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-list.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadList {\n public constructor(readonly directory: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadList, register: Register): Promise> {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.list(command.directory)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadList(input: {\n storageName: "clientst",\n directory: "client1"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadList": [\n {\n "name": "client1/myfile.txt",\n "properties": {\n "createdOn": "2022-10-26T05:40:47.000Z",\n "lastModified": "2022-10-26T05:40:47.000Z",\n "contentLength": 6,\n "contentType": "text/plain"\n }\n }\n ]\n }\n}\n'})})]}),(0,o.jsx)(r,{children:(0,o.jsxs)(n.p,{children:[(0,o.jsx)("summary",{children:"Delete File"}),"\nCurrently, the option to delete a file is only available on AWS. If this is a feature you were looking for, please let us know on Discord. Alternatively, you can implement this feature and submit a pull request on GitHub for this Rocket!"]})}),(0,o.jsx)(n.h2,{id:"azure-roles",children:"Azure Roles"}),(0,o.jsx)(n.admonition,{type:"info",children:(0,o.jsxs)(n.p,{children:["Starting at version ",(0,o.jsx)(n.strong,{children:"0.31.0"})," this Rocket use Managed Identities instead of Connection Strings. Please, check that you have the required permissions to assign roles ",(0,o.jsx)(n.a,{href:"https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal-managed-identity#prerequisites",children:"https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal-managed-identity#prerequisites"})]})}),(0,o.jsx)(n.p,{children:"For uploading files to Azure you need the Storage Blob Data Contributor role. This can be assigned to a user using the portal or with the next scripts:"}),(0,o.jsx)(n.p,{children:"First, check if you have the correct permissions:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'ACCOUNT_NAME=""\nCONTAINER_NAME=""\n\n# use this to test if you have the correct permissions\naz storage blob exists --account-name $ACCOUNT_NAME `\n --container-name $CONTAINER_NAME `\n --name blob1.txt --auth-mode login\n'})}),(0,o.jsx)(n.p,{children:"If you don't have it, then run this script as admin:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'ACCOUNT_NAME=""\nCONTAINER_NAME=""\n\nOBJECT_ID=$(az ad user list --query "[?mailNickname==\'\'].objectId" -o tsv)\nSTORAGE_ID=$(az storage account show -n $ACCOUNT_NAME --query id -o tsv)\n\naz role assignment create \\\n --role "Storage Blob Data Contributor" \\\n --assignee $OBJECT_ID \\\n --scope "$STORAGE_ID/blobServices/default/containers/$CONTAINER_NAME"\n'})})]}),(0,o.jsxs)(s.Z,{value:"aws-provider",label:"AWS Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"Install needed dependency packages:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save @boostercloud/rocket-file-uploads-core @boostercloud/rocket-file-uploads-types\nnpm install --save @boostercloud/rocket-file-uploads-aws\n"})}),(0,o.jsx)(n.p,{children:"Also, you will need a devDependency in your project:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save-dev @boostercloud/rocket-file-uploads-aws-infrastructure\n"})}),(0,o.jsx)(n.p,{children:"In your Booster config file, configure your BoosterRocketFiles:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/config/config.ts"',children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\nimport { BoosterRocketFiles } from '@boostercloud/rocket-file-uploads-core'\nimport { RocketFilesUserConfiguration } from '@boostercloud/rocket-file-uploads-types'\n\nconst rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {\n storageName: 'STORAGE_NAME',\n containerName: '', // Not used in AWS, you can just pass an empty string\n directories: ['DIRECTORY_1', 'DIRECTORY_2'],\n}\n\nconst rocketFilesConfigurationCms: RocketFilesUserConfiguration = {\n storageName: 'cmsst',\n containerName: '', // Not used in AWS, you can just pass an empty string\n directories: ['cms1', 'cms2'],\n}\n\nBooster.configure('production', (config: BoosterConfig): void => {\n config.appName = 'TEST_APP_NAME'\n config.providerPackage = '@boostercloud/framework-provider-aws'\n config.rockets = [\n new BoosterRocketFiles(config, [rocketFilesConfigurationDefault, rocketFilesConfigurationCms]).rocketForAWS(),\n ]\n})\n"})}),(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsx)(n.p,{children:"Available parameters are:"}),(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"storageName"}),": Name of the storage repository."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"directories"}),": A list of folders where the files will be stored."]}),"\n"]}),(0,o.jsx)(n.hr,{}),(0,o.jsx)(n.p,{children:"The structure created will be:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 storageName\n\u2502 \u251c\u2500\u2500 directory\n"})})]}),(0,o.jsx)(n.h2,{id:"rocket-methods-usage-1",children:"Rocket Methods Usage"}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Put"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedPut"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to upload on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-put.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadPut {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadPut, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedPut(command.directory, command.fileName) as Promise\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadPut(input: { \n directory: "files", \n fileName: "lol.jpg"\n }) {\n url\n fields\n }\n}\n'})}),(0,o.jsx)(n.p,{children:"AWS Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadPut": {\n "url": "https://s3.eu-west-1.amazonaws.com/myappstorage",\n "fields": {\n "Key": "files/lol.jpg",\n "bucket": "myappstorage",\n "X-Amz-Algorithm": "AWS4-HMAC-SHA256",\n "X-Amz-Credential": "blablabla.../eu-west-1/s3/aws4_request",\n "X-Amz-Date": "20230207T142138Z",\n "X-Amz-Security-Token": "IQoJb3JpZ2... blablabla",\n "Policy": "eyJleHBpcmF0a... blablabla",\n "X-Amz-Signature": "60511... blablabla"\n }\n }\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Get"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedGet"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to get on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-get.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadGet {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadGet, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedGet(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadGet(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"AWS Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadGet": "https://myappstorage.s3.eu-west-1.amazonaws.com/client1/myfile.txt?"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"List"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"list"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory you want to get the info and return the formatted results."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-list.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadList {\n public constructor(readonly directory: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadList, register: Register): Promise> {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.list(command.directory)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadList(input: {\n storageName: "clientst",\n directory: "client1"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadList": [\n {\n "name": "client1/myfile.txt",\n "properties": {\n "createdOn": "2022-10-26T05:40:47.000Z",\n "lastModified": "2022-10-26T05:40:47.000Z",\n "contentLength": 6,\n "contentType": "text/plain"\n }\n }\n ]\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Delete File"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"deleteFile"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and file name you want to delete."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/delete-file.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class DeleteFile {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: DeleteFile, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.deleteFile(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n DeleteFile(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "DeleteFile": true\n }\n}\n'})})]})]}),(0,o.jsxs)(s.Z,{value:"local-provider",label:"Local Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"Install needed dependency packages:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save @boostercloud/rocket-file-uploads-core @boostercloud/rocket-file-uploads-types\nnpm install --save @boostercloud/rocket-file-uploads-local\n"})}),(0,o.jsx)(n.p,{children:"Also, you will need a devDependency in your project:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"npm install --save-dev @boostercloud/rocket-file-uploads-local-infrastructure\n"})}),(0,o.jsx)(n.p,{children:"In your Booster config file, configure your BoosterRocketFiles:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/config/config.ts"',children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\nimport { BoosterRocketFiles } from '@boostercloud/rocket-file-uploads-core'\nimport { RocketFilesUserConfiguration } from '@boostercloud/rocket-file-uploads-types'\n\nconst rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {\n storageName: 'STORAGE_NAME',\n containerName: 'CONTAINER_NAME',\n directories: ['DIRECTORY_1', 'DIRECTORY_2'],\n}\n\nconst rocketFilesConfigurationCms: RocketFilesUserConfiguration = {\n storageName: 'cmsst',\n containerName: 'rocketfiles',\n directories: ['cms1', 'cms2'],\n}\n\nBooster.configure('local', (config: BoosterConfig): void => {\n config.appName = 'TEST_APP_NAME'\n config.providerPackage = '@boostercloud/framework-provider-local'\n config.rockets = [\n new BoosterRocketFiles(config, [rocketFilesConfigurationDefault, rocketFilesConfigurationCms]).rocketForLocal(),\n ]\n})\n"})}),(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsx)(n.p,{children:"Available parameters are:"}),(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"storageName"}),": Name of the storage repository."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"containerName"}),": Directories container."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"directories"}),": A list of folders where the files will be stored."]}),"\n"]}),(0,o.jsx)(n.hr,{}),(0,o.jsx)(n.p,{children:"The structure created will be:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 storageName\n\u2502 \u251c\u2500\u2500 containerName\n\u2502 \u2502 \u251c\u2500\u2500 directory\n"})}),(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"NOTE:"})," Local Provider will use ",(0,o.jsx)(n.code,{children:"storageName"})," as the root folder name."]})]}),(0,o.jsx)(n.h2,{id:"rocket-methods-usage-2",children:"Rocket Methods Usage"}),(0,o.jsxs)(r,{children:[(0,o.jsxs)(n.p,{children:[(0,o.jsx)("summary",{children:"Presigned Put"}),"\nCreate a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedPut"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to upload on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-put.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadPut {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadPut, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedPut(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadPut(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadPut": "http://localhost:3000/clientst/rocketfiles/client1/myfile.txt"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Get"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedGet"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to get on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-get.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadGet {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadGet, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedGet(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadGet(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadGet": "http://localhost:3000/clientst/rocketfiles/client1/myfile.txt"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"List"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"list"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory you want to get the info and return the formatted results."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-list.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadList {\n public constructor(readonly directory: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadList, register: Register): Promise> {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.list(command.directory)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadList(input: {\n storageName: "clientst",\n directory: "client1"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadList": [\n {\n "name": "client1/myfile.txt",\n "properties": {\n "lastModified": "2022-10-26T10:35:18.905Z"\n }\n }\n ]\n }\n}\n'})})]}),(0,o.jsx)(r,{children:(0,o.jsxs)(n.p,{children:[(0,o.jsx)("summary",{children:"Delete File"}),"\nCurrently, the option to delete a file is only available on AWS. If this is a feature you were looking for, please let us know on Discord. Alternatively, you can implement this feature and submit a pull request on GitHub for this Rocket!"]})}),(0,o.jsx)(n.h2,{id:"security",children:"Security"}),(0,o.jsx)(n.p,{children:"Local Provider doesn't check paths. You should check that the directory and files passed as paratemers are valid."})]})]}),"\n",(0,o.jsx)(n.hr,{}),"\n",(0,o.jsx)(n.h2,{id:"events",children:"Events"}),"\n",(0,o.jsxs)(n.p,{children:["For each uploaded file a new event will be automatically generated and properly reduced on the entity ",(0,o.jsx)(n.code,{children:"UploadedFileEntity"}),"."]}),"\n",(0,o.jsxs)(i.Z,{groupId:"providers-usage",children:[(0,o.jsxs)(s.Z,{value:"azure-and-aws-provider",label:"Azure & AWS Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"The event will look like this:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:'{\n "version": 1,\n "kind": "snapshot",\n "superKind": "domain",\n "requestID": "xxx",\n "entityID": "xxxx",\n "entityTypeName": "UploadedFileEntity",\n "typeName": "UploadedFileEntity",\n "value": {\n "id": "xxx",\n "metadata": {\n // A bunch of fields (depending on Azure or AWS)\n }\n },\n "createdAt": "2022-10-26T10:23:36.562Z",\n "snapshottedEventCreatedAt": "2022-10-26T10:23:32.34Z",\n "entityTypeName_entityID_kind": "UploadedFileEntity-xxx-b842-x-8975-xx-snapshot",\n "id": "x-x-x-x-x",\n "_rid": "x==",\n "_self": "dbs/x==/colls/x=/docs/x==/",\n "_etag": "\\"x-x-0500-0000-x\\"",\n "_attachments": "attachments/",\n "_ts": 123456\n}\n'})})]}),(0,o.jsxs)(s.Z,{value:"local-provider",label:"Local Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"The event will look like this:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:'{\n "version": 1,\n "kind": "snapshot",\n "superKind": "domain",\n "requestID": "x",\n "entityID": "x",\n "entityTypeName": "UploadedFileEntity",\n "typeName": "UploadedFileEntity",\n "value": {\n "id": "x",\n "metadata": {\n "uri": "http://localhost:3000/clientst/rocketfiles/client1/myfile.txt",\n "name": "client1/myfile.txt"\n }\n },\n "createdAt": "2022-10-26T10:35:18.967Z",\n "snapshottedEventCreatedAt": "2022-10-26T10:35:18.958Z",\n "_id": "lMolccTNJVojXiLz"\n}\n'})})]})]}),"\n",(0,o.jsx)(n.h2,{id:"todos",children:"TODOs"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"Add file deletion to Azure and Local (only supported in AWS at the moment)."}),"\n",(0,o.jsx)(n.li,{children:"Optional storage deletion when unmounting the stack."}),"\n",(0,o.jsx)(n.li,{children:"Optional events, in case you don't want to store that information in the events-store."}),"\n",(0,o.jsx)(n.li,{children:"When deleting a file, save a deletion event in the events-store. Only uploads are stored at the moment."}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(m,{...e})}):m(e)}},5162:(e,n,r)=>{r.d(n,{Z:()=>i});r(7294);var o=r(512);const t={tabItem:"tabItem_Ymn6"};var s=r(5893);function i(e){let{children:n,hidden:r,className:i}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,o.Z)(t.tabItem,i),hidden:r,children:n})}},4866:(e,n,r)=>{r.d(n,{Z:()=>k});var o=r(7294),t=r(512),s=r(2466),i=r(6550),a=r(469),l=r(1980),c=r(7392),d=r(12);function u(e){return o.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,o.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function m(e){const{values:n,children:r}=e;return(0,o.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:r,attributes:o,default:t}}=e;return{value:n,label:r,attributes:o,default:t}}))}(r);return function(e){const n=(0,c.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,r])}function p(e){let{value:n,tabValues:r}=e;return r.some((e=>e.value===n))}function h(e){let{queryString:n=!1,groupId:r}=e;const t=(0,i.k6)(),s=function(e){let{queryString:n=!1,groupId:r}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:n,groupId:r});return[(0,l._X)(s),(0,o.useCallback)((e=>{if(!s)return;const n=new URLSearchParams(t.location.search);n.set(s,e),t.replace({...t.location,search:n.toString()})}),[s,t])]}function g(e){const{defaultValue:n,queryString:r=!1,groupId:t}=e,s=m(e),[i,l]=(0,o.useState)((()=>function(e){let{defaultValue:n,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const o=r.find((e=>e.default))??r[0];if(!o)throw new Error("Unexpected error: 0 tabValues");return o.value}({defaultValue:n,tabValues:s}))),[c,u]=h({queryString:r,groupId:t}),[g,f]=function(e){let{groupId:n}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,s]=(0,d.Nk)(r);return[t,(0,o.useCallback)((e=>{r&&s.set(e)}),[r,s])]}({groupId:t}),x=(()=>{const e=c??g;return p({value:e,tabValues:s})?e:null})();(0,a.Z)((()=>{x&&l(x)}),[x]);return{selectedValue:i,selectValue:(0,o.useCallback)((e=>{if(!p({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),f(e)}),[u,f,s]),tabValues:s}}var f=r(2389);const x={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=r(5893);function y(e){let{className:n,block:r,selectedValue:o,selectValue:i,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,s.o5)(),d=e=>{const n=e.currentTarget,r=l.indexOf(n),t=a[r].value;t!==o&&(c(n),i(t))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=l.indexOf(e.currentTarget)+1;n=l[r]??l[0];break}case"ArrowLeft":{const r=l.indexOf(e.currentTarget)-1;n=l[r]??l[l.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.Z)("tabs",{"tabs--block":r},n),children:a.map((e=>{let{value:n,label:r,attributes:s}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:o===n?0:-1,"aria-selected":o===n,ref:e=>l.push(e),onKeyDown:u,onClick:d,...s,className:(0,t.Z)("tabs__item",x.tabItem,s?.className,{"tabs__item--active":o===n}),children:r??n},n)}))})}function b(e){let{lazy:n,children:r,selectedValue:t}=e;const s=(Array.isArray(r)?r:[r]).filter(Boolean);if(n){const e=s.find((e=>e.props.value===t));return e?(0,o.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:s.map(((e,n)=>(0,o.cloneElement)(e,{key:n,hidden:e.props.value!==t})))})}function N(e){const n=g(e);return(0,j.jsxs)("div",{className:(0,t.Z)("tabs-container",x.tabList),children:[(0,j.jsx)(y,{...e,...n}),(0,j.jsx)(b,{...e,...n})]})}function k(e){const n=(0,f.Z)();return(0,j.jsx)(N,{...e,children:u(e.children)},String(n))}},1151:(e,n,r)=>{r.d(n,{Z:()=>a,a:()=>i});var o=r(7294);const t={},s=o.createContext(t);function i(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1b08e8f8.f9279bc6.js b/assets/js/1b08e8f8.f9279bc6.js new file mode 100644 index 000000000..2dbdaca68 --- /dev/null +++ b/assets/js/1b08e8f8.f9279bc6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1588],{6637:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>u});var o=r(5893),t=r(1151),s=r(5162),i=r(4866);const a={},l="File Uploads Rocket",c={id:"going-deeper/rockets/rocket-file-uploads",title:"File Uploads Rocket",description:"This package is a configurable rocket to add a storage API to your Booster applications.",source:"@site/docs/10_going-deeper/rockets/rocket-file-uploads.md",sourceDirName:"10_going-deeper/rockets",slug:"/going-deeper/rockets/rocket-file-uploads",permalink:"/going-deeper/rockets/rocket-file-uploads",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/rockets/rocket-file-uploads.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Extending Booster with Rockets!",permalink:"/going-deeper/rockets"},next:{title:"Backup Booster Rocket",permalink:"/going-deeper/rockets/rocket-backup-booster"}},d={},u=[{value:"Supported Providers",id:"supported-providers",level:2},{value:"Overview",id:"overview",level:2},{value:"Usage",id:"usage",level:2},{value:"Rocket Methods Usage",id:"rocket-methods-usage",level:2},{value:"Azure Roles",id:"azure-roles",level:2},{value:"Rocket Methods Usage",id:"rocket-methods-usage-1",level:2},{value:"Rocket Methods Usage",id:"rocket-methods-usage-2",level:2},{value:"Security",id:"security",level:2},{value:"Events",id:"events",level:2},{value:"TODOs",id:"todos",level:2}];function m(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",hr:"hr",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.a)(),...e.components},{Details:r}=n;return r||function(e,n){throw new Error("Expected "+(n?"component":"object")+" `"+e+"` to be defined: you likely forgot to import, pass, or provide it.")}("Details",!0),(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"file-uploads-rocket",children:"File Uploads Rocket"}),"\n",(0,o.jsx)(n.p,{children:"This package is a configurable rocket to add a storage API to your Booster applications."}),"\n",(0,o.jsx)(n.admonition,{type:"info",children:(0,o.jsx)(n.p,{children:(0,o.jsx)(n.a,{href:"https://github.com/boostercloud/rocket-file-uploads",children:"GitHub Repo"})})}),"\n",(0,o.jsx)(n.h2,{id:"supported-providers",children:"Supported Providers"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"Azure Provider"}),"\n",(0,o.jsx)(n.li,{children:"AWS Provider"}),"\n",(0,o.jsx)(n.li,{children:"Local Provider"}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,o.jsx)(n.p,{children:"This rocket provides some methods to access files stores in your cloud provider:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"presignedPut"}),": Returns a presigned put url and the necessary form params. With this url files can be uploaded directly to your provider."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"presignedGet"}),": Returns a presigned get url to download a file. With this url files can be downloaded directly from your provider."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"list"}),": Returns a list of files stored in the provider."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"deleteFile"}),": Removes a file from a directory (only supported in AWS at the moment)."]}),"\n"]}),"\n",(0,o.jsx)(n.p,{children:"These methods may be used from a Command in your project secured via JWT Token.\nThis rocket also provides a Booster Event each time a file is uploaded."}),"\n",(0,o.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,o.jsxs)(i.Z,{groupId:"providers-usage",children:[(0,o.jsxs)(s.Z,{value:"azure-provider",label:"Azure Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"Install needed dependency packages:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save @boostercloud/rocket-file-uploads-core @boostercloud/rocket-file-uploads-types\nnpm install --save @boostercloud/rocket-file-uploads-azure\n"})}),(0,o.jsx)(n.p,{children:"Also, you will need a devDependency in your project:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save-dev @boostercloud/rocket-file-uploads-azure-infrastructure\n"})}),(0,o.jsx)(n.p,{children:"In your Booster config file, configure your BoosterRocketFiles:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/config/config.ts"',children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\nimport { BoosterRocketFiles } from '@boostercloud/rocket-file-uploads-core'\nimport { RocketFilesUserConfiguration } from '@boostercloud/rocket-file-uploads-types'\n\nconst rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {\n storageName: 'STORAGE_NAME',\n containerName: 'CONTAINER_NAME',\n directories: ['DIRECTORY_1', 'DIRECTORY_2'],\n}\n\nconst rocketFilesConfigurationCms: RocketFilesUserConfiguration = {\n storageName: 'cmsst',\n containerName: 'rocketfiles',\n directories: ['cms1', 'cms2'],\n}\n\nBooster.configure('production', (config: BoosterConfig): void => {\n config.appName = 'TEST_APP_NAME'\n config.providerPackage = '@boostercloud/framework-provider-azure'\n config.rockets = [\n new BoosterRocketFiles(config, [rocketFilesConfigurationDefault, rocketFilesConfigurationCms]).rocketForAzure(),\n ]\n})\n\n"})}),(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsx)(n.p,{children:"Available parameters are:"}),(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"storageName"}),": Name of the storage repository."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"containerName"}),": Directories container."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"directories"}),": A list of folders where the files will be stored."]}),"\n"]}),(0,o.jsx)(n.hr,{}),(0,o.jsx)(n.p,{children:"The structure created will be:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 storageName\n\u2502 \u251c\u2500\u2500 containerName\n\u2502 \u2502 \u251c\u2500\u2500 directory\n"})}),(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"NOTE:"})," Azure Provider will use ",(0,o.jsx)(n.code,{children:"storageName"})," as the Storage Account Name."]})]}),(0,o.jsx)(n.h2,{id:"rocket-methods-usage",children:"Rocket Methods Usage"}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Put"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedPut"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to upload on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-put.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadPut {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadPut, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedPut(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadPut(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Azure Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadPut": "https://clientst.blob.core.windows.net/rocketfiles/client1/myfile.txt?"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Get"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedGet"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to get on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-get.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadGet {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadGet, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedGet(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadGet(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Azure Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadGet": "https://clientst.blob.core.windows.net/rocketfiles/folder01%2Fmyfile.txt?"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"List"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"list"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory you want to get the info and return the formatted results."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-list.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadList {\n public constructor(readonly directory: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadList, register: Register): Promise> {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.list(command.directory)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadList(input: {\n storageName: "clientst",\n directory: "client1"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadList": [\n {\n "name": "client1/myfile.txt",\n "properties": {\n "createdOn": "2022-10-26T05:40:47.000Z",\n "lastModified": "2022-10-26T05:40:47.000Z",\n "contentLength": 6,\n "contentType": "text/plain"\n }\n }\n ]\n }\n}\n'})})]}),(0,o.jsx)(r,{children:(0,o.jsxs)(n.p,{children:[(0,o.jsx)("summary",{children:"Delete File"}),"\nCurrently, the option to delete a file is only available on AWS. If this is a feature you were looking for, please let us know on Discord. Alternatively, you can implement this feature and submit a pull request on GitHub for this Rocket!"]})}),(0,o.jsx)(n.h2,{id:"azure-roles",children:"Azure Roles"}),(0,o.jsx)(n.admonition,{type:"info",children:(0,o.jsxs)(n.p,{children:["Starting at version ",(0,o.jsx)(n.strong,{children:"0.31.0"})," this Rocket use Managed Identities instead of Connection Strings. Please, check that you have the required permissions to assign roles ",(0,o.jsx)(n.a,{href:"https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal-managed-identity#prerequisites",children:"https://learn.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal-managed-identity#prerequisites"})]})}),(0,o.jsx)(n.p,{children:"For uploading files to Azure you need the Storage Blob Data Contributor role. This can be assigned to a user using the portal or with the next scripts:"}),(0,o.jsx)(n.p,{children:"First, check if you have the correct permissions:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'ACCOUNT_NAME=""\nCONTAINER_NAME=""\n\n# use this to test if you have the correct permissions\naz storage blob exists --account-name $ACCOUNT_NAME `\n --container-name $CONTAINER_NAME `\n --name blob1.txt --auth-mode login\n'})}),(0,o.jsx)(n.p,{children:"If you don't have it, then run this script as admin:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'ACCOUNT_NAME=""\nCONTAINER_NAME=""\n\nOBJECT_ID=$(az ad user list --query "[?mailNickname==\'\'].objectId" -o tsv)\nSTORAGE_ID=$(az storage account show -n $ACCOUNT_NAME --query id -o tsv)\n\naz role assignment create \\\n --role "Storage Blob Data Contributor" \\\n --assignee $OBJECT_ID \\\n --scope "$STORAGE_ID/blobServices/default/containers/$CONTAINER_NAME"\n'})})]}),(0,o.jsxs)(s.Z,{value:"aws-provider",label:"AWS Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"Install needed dependency packages:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save @boostercloud/rocket-file-uploads-core @boostercloud/rocket-file-uploads-types\nnpm install --save @boostercloud/rocket-file-uploads-aws\n"})}),(0,o.jsx)(n.p,{children:"Also, you will need a devDependency in your project:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save-dev @boostercloud/rocket-file-uploads-aws-infrastructure\n"})}),(0,o.jsx)(n.p,{children:"In your Booster config file, configure your BoosterRocketFiles:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/config/config.ts"',children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\nimport { BoosterRocketFiles } from '@boostercloud/rocket-file-uploads-core'\nimport { RocketFilesUserConfiguration } from '@boostercloud/rocket-file-uploads-types'\n\nconst rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {\n storageName: 'STORAGE_NAME',\n containerName: '', // Not used in AWS, you can just pass an empty string\n directories: ['DIRECTORY_1', 'DIRECTORY_2'],\n}\n\nconst rocketFilesConfigurationCms: RocketFilesUserConfiguration = {\n storageName: 'cmsst',\n containerName: '', // Not used in AWS, you can just pass an empty string\n directories: ['cms1', 'cms2'],\n}\n\nBooster.configure('production', (config: BoosterConfig): void => {\n config.appName = 'TEST_APP_NAME'\n config.providerPackage = '@boostercloud/framework-provider-aws'\n config.rockets = [\n new BoosterRocketFiles(config, [rocketFilesConfigurationDefault, rocketFilesConfigurationCms]).rocketForAWS(),\n ]\n})\n"})}),(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsx)(n.p,{children:"Available parameters are:"}),(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"storageName"}),": Name of the storage repository."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"directories"}),": A list of folders where the files will be stored."]}),"\n"]}),(0,o.jsx)(n.hr,{}),(0,o.jsx)(n.p,{children:"The structure created will be:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 storageName\n\u2502 \u251c\u2500\u2500 directory\n"})})]}),(0,o.jsx)(n.h2,{id:"rocket-methods-usage-1",children:"Rocket Methods Usage"}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Put"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedPut"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to upload on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-put.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadPut {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadPut, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedPut(command.directory, command.fileName) as Promise\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadPut(input: { \n directory: "files", \n fileName: "lol.jpg"\n }) {\n url\n fields\n }\n}\n'})}),(0,o.jsx)(n.p,{children:"AWS Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadPut": {\n "url": "https://s3.eu-west-1.amazonaws.com/myappstorage",\n "fields": {\n "Key": "files/lol.jpg",\n "bucket": "myappstorage",\n "X-Amz-Algorithm": "AWS4-HMAC-SHA256",\n "X-Amz-Credential": "blablabla.../eu-west-1/s3/aws4_request",\n "X-Amz-Date": "20230207T142138Z",\n "X-Amz-Security-Token": "IQoJb3JpZ2... blablabla",\n "Policy": "eyJleHBpcmF0a... blablabla",\n "X-Amz-Signature": "60511... blablabla"\n }\n }\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Get"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedGet"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to get on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-get.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadGet {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadGet, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedGet(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadGet(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"AWS Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadGet": "https://myappstorage.s3.eu-west-1.amazonaws.com/client1/myfile.txt?"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"List"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"list"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory you want to get the info and return the formatted results."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-list.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadList {\n public constructor(readonly directory: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadList, register: Register): Promise> {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.list(command.directory)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadList(input: {\n storageName: "clientst",\n directory: "client1"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadList": [\n {\n "name": "client1/myfile.txt",\n "properties": {\n "createdOn": "2022-10-26T05:40:47.000Z",\n "lastModified": "2022-10-26T05:40:47.000Z",\n "contentLength": 6,\n "contentType": "text/plain"\n }\n }\n ]\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Delete File"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"deleteFile"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and file name you want to delete."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/delete-file.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class DeleteFile {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: DeleteFile, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.deleteFile(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n DeleteFile(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "DeleteFile": true\n }\n}\n'})})]})]}),(0,o.jsxs)(s.Z,{value:"local-provider",label:"Local Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"Install needed dependency packages:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:"npm install --save @boostercloud/rocket-file-uploads-core @boostercloud/rocket-file-uploads-types\nnpm install --save @boostercloud/rocket-file-uploads-local\n"})}),(0,o.jsx)(n.p,{children:"Also, you will need a devDependency in your project:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{children:"npm install --save-dev @boostercloud/rocket-file-uploads-local-infrastructure\n"})}),(0,o.jsx)(n.p,{children:"In your Booster config file, configure your BoosterRocketFiles:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/config/config.ts"',children:"import { Booster } from '@boostercloud/framework-core'\nimport { BoosterConfig } from '@boostercloud/framework-types'\nimport { BoosterRocketFiles } from '@boostercloud/rocket-file-uploads-core'\nimport { RocketFilesUserConfiguration } from '@boostercloud/rocket-file-uploads-types'\n\nconst rocketFilesConfigurationDefault: RocketFilesUserConfiguration = {\n storageName: 'STORAGE_NAME',\n containerName: 'CONTAINER_NAME',\n directories: ['DIRECTORY_1', 'DIRECTORY_2'],\n}\n\nconst rocketFilesConfigurationCms: RocketFilesUserConfiguration = {\n storageName: 'cmsst',\n containerName: 'rocketfiles',\n directories: ['cms1', 'cms2'],\n}\n\nBooster.configure('local', (config: BoosterConfig): void => {\n config.appName = 'TEST_APP_NAME'\n config.providerPackage = '@boostercloud/framework-provider-local'\n config.rockets = [\n new BoosterRocketFiles(config, [rocketFilesConfigurationDefault, rocketFilesConfigurationCms]).rocketForLocal(),\n ]\n})\n"})}),(0,o.jsxs)(n.admonition,{type:"info",children:[(0,o.jsx)(n.p,{children:"Available parameters are:"}),(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"storageName"}),": Name of the storage repository."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"containerName"}),": Directories container."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"directories"}),": A list of folders where the files will be stored."]}),"\n"]}),(0,o.jsx)(n.hr,{}),(0,o.jsx)(n.p,{children:"The structure created will be:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 storageName\n\u2502 \u251c\u2500\u2500 containerName\n\u2502 \u2502 \u251c\u2500\u2500 directory\n"})}),(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"NOTE:"})," Local Provider will use ",(0,o.jsx)(n.code,{children:"storageName"})," as the root folder name."]})]}),(0,o.jsx)(n.h2,{id:"rocket-methods-usage-2",children:"Rocket Methods Usage"}),(0,o.jsxs)(r,{children:[(0,o.jsxs)(n.p,{children:[(0,o.jsx)("summary",{children:"Presigned Put"}),"\nCreate a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedPut"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to upload on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-put.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadPut {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadPut, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedPut(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadPut(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadPut": "http://localhost:3000/clientst/rocketfiles/client1/myfile.txt"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"Presigned Get"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"presignedGet"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory and filename you want to get on the storage."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-get.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadGet {\n public constructor(readonly directory: string, readonly fileName: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadGet, register: Register): Promise {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.presignedGet(command.directory, command.fileName)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadGet(input: {\n storageName: "clientst",\n directory: "client1",\n fileName: "myfile.txt"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadGet": "http://localhost:3000/clientst/rocketfiles/client1/myfile.txt"\n }\n}\n'})})]}),(0,o.jsxs)(r,{children:[(0,o.jsx)("summary",{children:"List"}),(0,o.jsxs)(n.p,{children:["Create a command in your application and call the ",(0,o.jsx)(n.code,{children:"list"})," method on the ",(0,o.jsx)(n.code,{children:"FileHandler"})," class with the directory you want to get the info and return the formatted results."]}),(0,o.jsx)(n.p,{children:"The storageName parameter is optional. It will use the first storage if undefined."}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/file-upload-list.ts"',children:"import { Booster, Command } from '@boostercloud/framework-core'\nimport { Register } from '@boostercloud/framework-types'\nimport { FileHandler } from '@boostercloud/rocket-file-uploads-core'\nimport { ListItem } from '@boostercloud/rocket-file-uploads-types'\n\n@Command({\n authorize: 'all',\n})\nexport class FileUploadList {\n public constructor(readonly directory: string, readonly storageName?: string) {}\n\n public static async handle(command: FileUploadList, register: Register): Promise> {\n const boosterConfig = Booster.config\n const fileHandler = new FileHandler(boosterConfig, command.storageName)\n return await fileHandler.list(command.directory)\n }\n}\n"})}),(0,o.jsx)(n.p,{children:"GraphQL Mutation:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'mutation {\n FileUploadList(input: {\n storageName: "clientst",\n directory: "client1"\n }\n )\n}\n'})}),(0,o.jsx)(n.p,{children:"Response:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "FileUploadList": [\n {\n "name": "client1/myfile.txt",\n "properties": {\n "lastModified": "2022-10-26T10:35:18.905Z"\n }\n }\n ]\n }\n}\n'})})]}),(0,o.jsx)(r,{children:(0,o.jsxs)(n.p,{children:[(0,o.jsx)("summary",{children:"Delete File"}),"\nCurrently, the option to delete a file is only available on AWS. If this is a feature you were looking for, please let us know on Discord. Alternatively, you can implement this feature and submit a pull request on GitHub for this Rocket!"]})}),(0,o.jsx)(n.h2,{id:"security",children:"Security"}),(0,o.jsx)(n.p,{children:"Local Provider doesn't check paths. You should check that the directory and files passed as paratemers are valid."})]})]}),"\n",(0,o.jsx)(n.hr,{}),"\n",(0,o.jsx)(n.h2,{id:"events",children:"Events"}),"\n",(0,o.jsxs)(n.p,{children:["For each uploaded file a new event will be automatically generated and properly reduced on the entity ",(0,o.jsx)(n.code,{children:"UploadedFileEntity"}),"."]}),"\n",(0,o.jsxs)(i.Z,{groupId:"providers-usage",children:[(0,o.jsxs)(s.Z,{value:"azure-and-aws-provider",label:"Azure & AWS Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"The event will look like this:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:'{\n "version": 1,\n "kind": "snapshot",\n "superKind": "domain",\n "requestID": "xxx",\n "entityID": "xxxx",\n "entityTypeName": "UploadedFileEntity",\n "typeName": "UploadedFileEntity",\n "value": {\n "id": "xxx",\n "metadata": {\n // A bunch of fields (depending on Azure or AWS)\n }\n },\n "createdAt": "2022-10-26T10:23:36.562Z",\n "snapshottedEventCreatedAt": "2022-10-26T10:23:32.34Z",\n "entityTypeName_entityID_kind": "UploadedFileEntity-xxx-b842-x-8975-xx-snapshot",\n "id": "x-x-x-x-x",\n "_rid": "x==",\n "_self": "dbs/x==/colls/x=/docs/x==/",\n "_etag": "\\"x-x-0500-0000-x\\"",\n "_attachments": "attachments/",\n "_ts": 123456\n}\n'})})]}),(0,o.jsxs)(s.Z,{value:"local-provider",label:"Local Provider",default:!0,children:[(0,o.jsx)(n.p,{children:"The event will look like this:"}),(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:'{\n "version": 1,\n "kind": "snapshot",\n "superKind": "domain",\n "requestID": "x",\n "entityID": "x",\n "entityTypeName": "UploadedFileEntity",\n "typeName": "UploadedFileEntity",\n "value": {\n "id": "x",\n "metadata": {\n "uri": "http://localhost:3000/clientst/rocketfiles/client1/myfile.txt",\n "name": "client1/myfile.txt"\n }\n },\n "createdAt": "2022-10-26T10:35:18.967Z",\n "snapshottedEventCreatedAt": "2022-10-26T10:35:18.958Z",\n "_id": "lMolccTNJVojXiLz"\n}\n'})})]})]}),"\n",(0,o.jsx)(n.h2,{id:"todos",children:"TODOs"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:"Add file deletion to Azure and Local (only supported in AWS at the moment)."}),"\n",(0,o.jsx)(n.li,{children:"Optional storage deletion when unmounting the stack."}),"\n",(0,o.jsx)(n.li,{children:"Optional events, in case you don't want to store that information in the events-store."}),"\n",(0,o.jsx)(n.li,{children:"When deleting a file, save a deletion event in the events-store. Only uploads are stored at the moment."}),"\n"]})]})}function p(e={}){const{wrapper:n}={...(0,t.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(m,{...e})}):m(e)}},5162:(e,n,r)=>{r.d(n,{Z:()=>i});r(7294);var o=r(512);const t={tabItem:"tabItem_Ymn6"};var s=r(5893);function i(e){let{children:n,hidden:r,className:i}=e;return(0,s.jsx)("div",{role:"tabpanel",className:(0,o.Z)(t.tabItem,i),hidden:r,children:n})}},4866:(e,n,r)=>{r.d(n,{Z:()=>k});var o=r(7294),t=r(512),s=r(2466),i=r(6550),a=r(469),l=r(1980),c=r(7392),d=r(12);function u(e){return o.Children.toArray(e).filter((e=>"\n"!==e)).map((e=>{if(!e||(0,o.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)}))?.filter(Boolean)??[]}function m(e){const{values:n,children:r}=e;return(0,o.useMemo)((()=>{const e=n??function(e){return u(e).map((e=>{let{props:{value:n,label:r,attributes:o,default:t}}=e;return{value:n,label:r,attributes:o,default:t}}))}(r);return function(e){const n=(0,c.l)(e,((e,n)=>e.value===n.value));if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map((e=>e.value)).join(", ")}" found in . Every value needs to be unique.`)}(e),e}),[n,r])}function p(e){let{value:n,tabValues:r}=e;return r.some((e=>e.value===n))}function h(e){let{queryString:n=!1,groupId:r}=e;const t=(0,i.k6)(),s=function(e){let{queryString:n=!1,groupId:r}=e;if("string"==typeof n)return n;if(!1===n)return null;if(!0===n&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:n,groupId:r});return[(0,l._X)(s),(0,o.useCallback)((e=>{if(!s)return;const n=new URLSearchParams(t.location.search);n.set(s,e),t.replace({...t.location,search:n.toString()})}),[s,t])]}function g(e){const{defaultValue:n,queryString:r=!1,groupId:t}=e,s=m(e),[i,l]=(0,o.useState)((()=>function(e){let{defaultValue:n,tabValues:r}=e;if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(n){if(!p({value:n,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${n}" but none of its children has the corresponding value. Available values are: ${r.map((e=>e.value)).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return n}const o=r.find((e=>e.default))??r[0];if(!o)throw new Error("Unexpected error: 0 tabValues");return o.value}({defaultValue:n,tabValues:s}))),[c,u]=h({queryString:r,groupId:t}),[g,f]=function(e){let{groupId:n}=e;const r=function(e){return e?`docusaurus.tab.${e}`:null}(n),[t,s]=(0,d.Nk)(r);return[t,(0,o.useCallback)((e=>{r&&s.set(e)}),[r,s])]}({groupId:t}),x=(()=>{const e=c??g;return p({value:e,tabValues:s})?e:null})();(0,a.Z)((()=>{x&&l(x)}),[x]);return{selectedValue:i,selectValue:(0,o.useCallback)((e=>{if(!p({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),f(e)}),[u,f,s]),tabValues:s}}var f=r(2389);const x={tabList:"tabList__CuJ",tabItem:"tabItem_LNqP"};var j=r(5893);function y(e){let{className:n,block:r,selectedValue:o,selectValue:i,tabValues:a}=e;const l=[],{blockElementScrollPositionUntilNextRender:c}=(0,s.o5)(),d=e=>{const n=e.currentTarget,r=l.indexOf(n),t=a[r].value;t!==o&&(c(n),i(t))},u=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=l.indexOf(e.currentTarget)+1;n=l[r]??l[0];break}case"ArrowLeft":{const r=l.indexOf(e.currentTarget)-1;n=l[r]??l[l.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.Z)("tabs",{"tabs--block":r},n),children:a.map((e=>{let{value:n,label:r,attributes:s}=e;return(0,j.jsx)("li",{role:"tab",tabIndex:o===n?0:-1,"aria-selected":o===n,ref:e=>l.push(e),onKeyDown:u,onClick:d,...s,className:(0,t.Z)("tabs__item",x.tabItem,s?.className,{"tabs__item--active":o===n}),children:r??n},n)}))})}function b(e){let{lazy:n,children:r,selectedValue:t}=e;const s=(Array.isArray(r)?r:[r]).filter(Boolean);if(n){const e=s.find((e=>e.props.value===t));return e?(0,o.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:s.map(((e,n)=>(0,o.cloneElement)(e,{key:n,hidden:e.props.value!==t})))})}function N(e){const n=g(e);return(0,j.jsxs)("div",{className:(0,t.Z)("tabs-container",x.tabList),children:[(0,j.jsx)(y,{...e,...n}),(0,j.jsx)(b,{...e,...n})]})}function k(e){const n=(0,f.Z)();return(0,j.jsx)(N,{...e,children:u(e.children)},String(n))}},1151:(e,n,r)=>{r.d(n,{Z:()=>a,a:()=>i});var o=r(7294);const t={},s=o.createContext(t);function i(e){const n=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:i(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1efc9436.f5bfb109.js b/assets/js/1efc9436.f5bfb109.js deleted file mode 100644 index 54f943cd2..000000000 --- a/assets/js/1efc9436.f5bfb109.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[2126],{1829:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>o,metadata:()=>d,toc:()=>l});var i=n(5893),r=n(1151),s=n(5163);const o={},a="Entity",d={id:"architecture/entity",title:"Entity",description:"If events are the source of truth of your application, entities are the current state of your application. For example, if you have an application that allows users to create bank accounts, the events would be something like AccountCreated, MoneyDeposited, MoneyWithdrawn, etc. But the entities would be the BankAccount themselves, with the current balance, owner, etc.",source:"@site/docs/03_architecture/05_entity.mdx",sourceDirName:"03_architecture",slug:"/architecture/entity",permalink:"/architecture/entity",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/05_entity.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:5,frontMatter:{},sidebar:"docs",previous:{title:"Event handler",permalink:"/architecture/event-handler"},next:{title:"Read model",permalink:"/architecture/read-model"}},c={},l=[{value:"Creating entities",id:"creating-entities",level:2},{value:"Declaring an entity",id:"declaring-an-entity",level:2},{value:"The reduce function",id:"the-reduce-function",level:2},{value:"Reducing multiple events",id:"reducing-multiple-events",level:3},{value:"Eventual Consistency",id:"eventual-consistency",level:3},{value:"Entity ID",id:"entity-id",level:2},{value:"Entities naming convention",id:"entities-naming-convention",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"entity",children:"Entity"}),"\n",(0,i.jsxs)(t.p,{children:["If events are the ",(0,i.jsx)(t.em,{children:"source of truth"})," of your application, entities are the ",(0,i.jsx)(t.em,{children:"current state"})," of your application. For example, if you have an application that allows users to create bank accounts, the events would be something like ",(0,i.jsx)(t.code,{children:"AccountCreated"}),", ",(0,i.jsx)(t.code,{children:"MoneyDeposited"}),", ",(0,i.jsx)(t.code,{children:"MoneyWithdrawn"}),", etc. But the entities would be the ",(0,i.jsx)(t.code,{children:"BankAccount"})," themselves, with the current balance, owner, etc."]}),"\n",(0,i.jsxs)(t.p,{children:["Entities are created by ",(0,i.jsx)(t.em,{children:"reducing"})," the whole event stream. Booster generates entities on the fly, so you don't have to worry about their creation. However, you must define them in order to instruct Booster how to generate them."]}),"\n",(0,i.jsx)(t.admonition,{type:"info",children:(0,i.jsx)(t.p,{children:"Under the hood, Booster stores snapshots of the entities in order to reduce the load on the event store. That way, Booster doesn't have to reduce the whole event stream whenever the current state of an entity is needed."})}),"\n",(0,i.jsx)(t.h2,{id:"creating-entities",children:"Creating entities"}),"\n",(0,i.jsx)(t.p,{children:"The Booster CLI will help you to create new entities. You just need to run the following command and the CLI will generate all the boilerplate for you:"}),"\n",(0,i.jsx)(s.Z,{children:(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"boost new:entity Product --fields displayName:string description:string price:Money\n"})})}),"\n",(0,i.jsxs)(t.p,{children:["This will generate a new file called ",(0,i.jsx)(t.code,{children:"product.ts"})," in the ",(0,i.jsx)(t.code,{children:"src/entities"})," directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI."]}),"\n",(0,i.jsx)(t.h2,{id:"declaring-an-entity",children:"Declaring an entity"}),"\n",(0,i.jsxs)(t.p,{children:["To declare an entity in Booster, you must define a class decorated with the ",(0,i.jsx)(t.code,{children:"@Entity"})," decorator. Inside of the class, you must define a constructor with all the fields you want to have in your entity."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/entity-name.ts"',children:"@Entity\nexport class EntityName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType /* as many fields as needed */) {}\n}\n"})}),"\n",(0,i.jsx)(t.h2,{id:"the-reduce-function",children:"The reduce function"}),"\n",(0,i.jsxs)(t.p,{children:["In order to tell Booster how to reduce the events, you must define a static method decorated with the ",(0,i.jsx)(t.code,{children:"@Reduces"})," decorator. This method will be called by the framework every time an event of the specified type is emitted. The reducer method must return a new entity instance with the current state of the entity."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/entity-name.ts"',children:"@Entity\nexport class EntityName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType /* as many fields as needed */) {}\n\n // highlight-start\n @Reduces(SomeEvent)\n public static reduceSomeEvent(event: SomeEvent, currentEntityState?: EntityName): EntityName {\n /* Return a new entity based on the current one */\n }\n // highlight-end\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"The reducer method receives two parameters:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"event"})," - The event object that triggered the reducer"]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"currentEntity?"})," - The current state of the entity instance that the event belongs to if it exists. ",(0,i.jsx)(t.strong,{children:"This parameter is optional"})," and will be ",(0,i.jsx)(t.code,{children:"undefined"})," if the entity doesn't exist yet (For example, when you process a ",(0,i.jsx)(t.code,{children:"ProductCreated"})," event that will generate the first version of a ",(0,i.jsx)(t.code,{children:"Product"})," entity)."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"reducing-multiple-events",children:"Reducing multiple events"}),"\n",(0,i.jsxs)(t.p,{children:["You can define as many reducer methods as you want, each one for a different event type. For example, if you have a ",(0,i.jsx)(t.code,{children:"Cart"})," entity, you could define a reducer for ",(0,i.jsx)(t.code,{children:"ProductAdded"})," events and another one for ",(0,i.jsx)(t.code,{children:"ProductRemoved"})," events."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/cart.ts"',children:"@Entity\nexport class Cart {\n public constructor(readonly items: Array) {}\n\n @Reduces(ProductAdded)\n public static reduceProductAdded(event: ProductAdded, currentCart?: Cart): Cart {\n const newItems = addToCart(event.item, currentCart)\n return new Cart(newItems)\n }\n\n @Reduces(ProductRemoved)\n public static reduceProductRemoved(event: ProductRemoved, currentCart?: Cart): Cart {\n const newItems = removeFromCart(event.item, currentCart)\n return new Cart(newItems)\n }\n}\n"})}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["It's highly recommended to ",(0,i.jsx)(t.strong,{children:"keep your reducer functions pure"}),", which means that you should be able to produce the new entity version by just looking at the event and the current entity state. You should avoid calling third party services, reading or writing to a database, or changing any external state."]})}),"\n",(0,i.jsxs)(t.p,{children:["There could be a lot of events being reduced concurrently among many entities, but, ",(0,i.jsx)(t.strong,{children:"for a specific entity instance, the events order is preserved"}),". This means that while one event is being reduced, all other events of any kind ",(0,i.jsx)(t.em,{children:"that belong to the same entity instance"})," will be waiting in a queue until the previous reducer has finished. This is how Booster guarantees that the entity state is consistent."]}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"reducer process gif",src:n(5876).Z+"",width:"1208",height:"638"})}),"\n",(0,i.jsx)(t.h3,{id:"eventual-consistency",children:"Eventual Consistency"}),"\n",(0,i.jsxs)(t.p,{children:["Additionally, due to the event driven and async nature of Booster, your data might not be instantly updated. Booster will consume the commands, generate events, and ",(0,i.jsx)(t.em,{children:"eventually"})," generate the entities. Most of the time this is not perceivable, but under huge loads, it could be noticed."]}),"\n",(0,i.jsxs)(t.p,{children:["This property is called ",(0,i.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/Eventual_consistency",children:"Eventual Consistency"}),", and it is a trade-off to have high availability for extreme situations, where other systems might simply fail."]}),"\n",(0,i.jsx)(t.h2,{id:"entity-id",children:"Entity ID"}),"\n",(0,i.jsxs)(t.p,{children:["In order to identify each entity instance, you must define an ",(0,i.jsx)(t.code,{children:"id"})," field on each entity. This field will be used by the framework to identify the entity instance. If the value of the ",(0,i.jsx)(t.code,{children:"id"})," field matches the value returned by the ",(0,i.jsxs)(t.a,{href:"event#events-and-entities",children:[(0,i.jsx)(t.code,{children:"entityID()"})," method"]})," of an Event, the framework will consider that the event belongs to that entity instance."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/entity-name.ts"',children:"@Entity\nexport class EntityName {\n public constructor(\n // highlight-next-line\n readonly id: UUID,\n readonly fieldA: SomeType,\n readonly fieldB: SomeOtherType /* as many fields as needed */\n ) {}\n\n @Reduces(SomeEvent)\n public static reduceSomeEvent(event: SomeEvent, currentEntityState?: EntityName): EntityName {\n /* Return a new entity based on the current one */\n }\n}\n"})}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["We recommend you to use the ",(0,i.jsx)(t.code,{children:"UUID"})," type for the ",(0,i.jsx)(t.code,{children:"id"})," field. You can generate a new ",(0,i.jsx)(t.code,{children:"UUID"})," value by calling the ",(0,i.jsx)(t.code,{children:"UUID.generate()"})," method already provided by the framework."]})}),"\n",(0,i.jsx)(t.h2,{id:"entities-naming-convention",children:"Entities naming convention"}),"\n",(0,i.jsx)(t.p,{children:"Entities are a representation of your application state in a specific moment, so name them as closely to your domain objects as possible. Typical entity names are nouns that might appear when you think about your app. In an e-commerce application, some entities would be:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Cart"}),"\n",(0,i.jsx)(t.li,{children:"Product"}),"\n",(0,i.jsx)(t.li,{children:"UserProfile"}),"\n",(0,i.jsx)(t.li,{children:"Order"}),"\n",(0,i.jsx)(t.li,{children:"Address"}),"\n",(0,i.jsx)(t.li,{children:"PaymentMethod"}),"\n",(0,i.jsx)(t.li,{children:"Stock"}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Entities live within the entities directory of the project source: ",(0,i.jsx)(t.code,{children:"/src/entities"}),"."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-text",children:"\n\u251c\u2500\u2500 src\n\u2502 \u251c\u2500\u2500 commands\n\u2502 \u251c\u2500\u2500 common\n\u2502 \u251c\u2500\u2500 config\n\u2502 \u251c\u2500\u2500 entities <------ put them here\n\u2502 \u251c\u2500\u2500 events\n\u2502 \u251c\u2500\u2500 index.ts\n\u2502 \u2514\u2500\u2500 read-models\n"})})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},5163:(e,t,n)=>{n.d(t,{Z:()=>s});n(7294);const i={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var r=n(5893);function s(e){let{children:t}=e;return(0,r.jsxs)("div",{className:i.terminalWindow,children:[(0,r.jsx)("div",{className:i.terminalWindowHeader,children:(0,r.jsxs)("div",{className:i.buttons,children:[(0,r.jsx)("span",{className:i.dot,style:{background:"#f25f58"}}),(0,r.jsx)("span",{className:i.dot,style:{background:"#fbbe3c"}}),(0,r.jsx)("span",{className:i.dot,style:{background:"#58cb42"}})]})}),(0,r.jsx)("div",{className:i.terminalWindowBody,children:t})]})}},5876:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/reducer-faf967cd976ea38d84e14551aa3af383.gif"},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>o});var i=n(7294);const r={},s=i.createContext(r);function o(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1efc9436.f6dbd1db.js b/assets/js/1efc9436.f6dbd1db.js new file mode 100644 index 000000000..37cb8ccd1 --- /dev/null +++ b/assets/js/1efc9436.f6dbd1db.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[2126],{1829:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>o,metadata:()=>d,toc:()=>l});var i=n(5893),r=n(1151),s=n(5163);const o={},a="Entity",d={id:"architecture/entity",title:"Entity",description:"If events are the source of truth of your application, entities are the current state of your application. For example, if you have an application that allows users to create bank accounts, the events would be something like AccountCreated, MoneyDeposited, MoneyWithdrawn, etc. But the entities would be the BankAccount themselves, with the current balance, owner, etc.",source:"@site/docs/03_architecture/05_entity.mdx",sourceDirName:"03_architecture",slug:"/architecture/entity",permalink:"/architecture/entity",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/05_entity.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:5,frontMatter:{},sidebar:"docs",previous:{title:"Event handler",permalink:"/architecture/event-handler"},next:{title:"Read model",permalink:"/architecture/read-model"}},c={},l=[{value:"Creating entities",id:"creating-entities",level:2},{value:"Declaring an entity",id:"declaring-an-entity",level:2},{value:"The reduce function",id:"the-reduce-function",level:2},{value:"Reducing multiple events",id:"reducing-multiple-events",level:3},{value:"Eventual Consistency",id:"eventual-consistency",level:3},{value:"Entity ID",id:"entity-id",level:2},{value:"Entities naming convention",id:"entities-naming-convention",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"entity",children:"Entity"}),"\n",(0,i.jsxs)(t.p,{children:["If events are the ",(0,i.jsx)(t.em,{children:"source of truth"})," of your application, entities are the ",(0,i.jsx)(t.em,{children:"current state"})," of your application. For example, if you have an application that allows users to create bank accounts, the events would be something like ",(0,i.jsx)(t.code,{children:"AccountCreated"}),", ",(0,i.jsx)(t.code,{children:"MoneyDeposited"}),", ",(0,i.jsx)(t.code,{children:"MoneyWithdrawn"}),", etc. But the entities would be the ",(0,i.jsx)(t.code,{children:"BankAccount"})," themselves, with the current balance, owner, etc."]}),"\n",(0,i.jsxs)(t.p,{children:["Entities are created by ",(0,i.jsx)(t.em,{children:"reducing"})," the whole event stream. Booster generates entities on the fly, so you don't have to worry about their creation. However, you must define them in order to instruct Booster how to generate them."]}),"\n",(0,i.jsx)(t.admonition,{type:"info",children:(0,i.jsx)(t.p,{children:"Under the hood, Booster stores snapshots of the entities in order to reduce the load on the event store. That way, Booster doesn't have to reduce the whole event stream whenever the current state of an entity is needed."})}),"\n",(0,i.jsx)(t.h2,{id:"creating-entities",children:"Creating entities"}),"\n",(0,i.jsx)(t.p,{children:"The Booster CLI will help you to create new entities. You just need to run the following command and the CLI will generate all the boilerplate for you:"}),"\n",(0,i.jsx)(s.Z,{children:(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-shell",children:"boost new:entity Product --fields displayName:string description:string price:Money\n"})})}),"\n",(0,i.jsxs)(t.p,{children:["This will generate a new file called ",(0,i.jsx)(t.code,{children:"product.ts"})," in the ",(0,i.jsx)(t.code,{children:"src/entities"})," directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI."]}),"\n",(0,i.jsx)(t.h2,{id:"declaring-an-entity",children:"Declaring an entity"}),"\n",(0,i.jsxs)(t.p,{children:["To declare an entity in Booster, you must define a class decorated with the ",(0,i.jsx)(t.code,{children:"@Entity"})," decorator. Inside of the class, you must define a constructor with all the fields you want to have in your entity."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/entity-name.ts"',children:"@Entity\nexport class EntityName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType /* as many fields as needed */) {}\n}\n"})}),"\n",(0,i.jsx)(t.h2,{id:"the-reduce-function",children:"The reduce function"}),"\n",(0,i.jsxs)(t.p,{children:["In order to tell Booster how to reduce the events, you must define a static method decorated with the ",(0,i.jsx)(t.code,{children:"@Reduces"})," decorator. This method will be called by the framework every time an event of the specified type is emitted. The reducer method must return a new entity instance with the current state of the entity."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/entity-name.ts"',children:"@Entity\nexport class EntityName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType /* as many fields as needed */) {}\n\n // highlight-start\n @Reduces(SomeEvent)\n public static reduceSomeEvent(event: SomeEvent, currentEntityState?: EntityName): EntityName {\n /* Return a new entity based on the current one */\n }\n // highlight-end\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"The reducer method receives two parameters:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"event"})," - The event object that triggered the reducer"]}),"\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.code,{children:"currentEntity?"})," - The current state of the entity instance that the event belongs to if it exists. ",(0,i.jsx)(t.strong,{children:"This parameter is optional"})," and will be ",(0,i.jsx)(t.code,{children:"undefined"})," if the entity doesn't exist yet (For example, when you process a ",(0,i.jsx)(t.code,{children:"ProductCreated"})," event that will generate the first version of a ",(0,i.jsx)(t.code,{children:"Product"})," entity)."]}),"\n"]}),"\n",(0,i.jsx)(t.h3,{id:"reducing-multiple-events",children:"Reducing multiple events"}),"\n",(0,i.jsxs)(t.p,{children:["You can define as many reducer methods as you want, each one for a different event type. For example, if you have a ",(0,i.jsx)(t.code,{children:"Cart"})," entity, you could define a reducer for ",(0,i.jsx)(t.code,{children:"ProductAdded"})," events and another one for ",(0,i.jsx)(t.code,{children:"ProductRemoved"})," events."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/cart.ts"',children:"@Entity\nexport class Cart {\n public constructor(readonly items: Array) {}\n\n @Reduces(ProductAdded)\n public static reduceProductAdded(event: ProductAdded, currentCart?: Cart): Cart {\n const newItems = addToCart(event.item, currentCart)\n return new Cart(newItems)\n }\n\n @Reduces(ProductRemoved)\n public static reduceProductRemoved(event: ProductRemoved, currentCart?: Cart): Cart {\n const newItems = removeFromCart(event.item, currentCart)\n return new Cart(newItems)\n }\n}\n"})}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["It's highly recommended to ",(0,i.jsx)(t.strong,{children:"keep your reducer functions pure"}),", which means that you should be able to produce the new entity version by just looking at the event and the current entity state. You should avoid calling third party services, reading or writing to a database, or changing any external state."]})}),"\n",(0,i.jsxs)(t.p,{children:["There could be a lot of events being reduced concurrently among many entities, but, ",(0,i.jsx)(t.strong,{children:"for a specific entity instance, the events order is preserved"}),". This means that while one event is being reduced, all other events of any kind ",(0,i.jsx)(t.em,{children:"that belong to the same entity instance"})," will be waiting in a queue until the previous reducer has finished. This is how Booster guarantees that the entity state is consistent."]}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"reducer process gif",src:n(5876).Z+"",width:"1208",height:"638"})}),"\n",(0,i.jsx)(t.h3,{id:"eventual-consistency",children:"Eventual Consistency"}),"\n",(0,i.jsxs)(t.p,{children:["Additionally, due to the event driven and async nature of Booster, your data might not be instantly updated. Booster will consume the commands, generate events, and ",(0,i.jsx)(t.em,{children:"eventually"})," generate the entities. Most of the time this is not perceivable, but under huge loads, it could be noticed."]}),"\n",(0,i.jsxs)(t.p,{children:["This property is called ",(0,i.jsx)(t.a,{href:"https://en.wikipedia.org/wiki/Eventual_consistency",children:"Eventual Consistency"}),", and it is a trade-off to have high availability for extreme situations, where other systems might simply fail."]}),"\n",(0,i.jsx)(t.h2,{id:"entity-id",children:"Entity ID"}),"\n",(0,i.jsxs)(t.p,{children:["In order to identify each entity instance, you must define an ",(0,i.jsx)(t.code,{children:"id"})," field on each entity. This field will be used by the framework to identify the entity instance. If the value of the ",(0,i.jsx)(t.code,{children:"id"})," field matches the value returned by the ",(0,i.jsxs)(t.a,{href:"event#events-and-entities",children:[(0,i.jsx)(t.code,{children:"entityID()"})," method"]})," of an Event, the framework will consider that the event belongs to that entity instance."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-typescript",metastring:'title="src/entities/entity-name.ts"',children:"@Entity\nexport class EntityName {\n public constructor(\n // highlight-next-line\n readonly id: UUID,\n readonly fieldA: SomeType,\n readonly fieldB: SomeOtherType /* as many fields as needed */\n ) {}\n\n @Reduces(SomeEvent)\n public static reduceSomeEvent(event: SomeEvent, currentEntityState?: EntityName): EntityName {\n /* Return a new entity based on the current one */\n }\n}\n"})}),"\n",(0,i.jsx)(t.admonition,{type:"tip",children:(0,i.jsxs)(t.p,{children:["We recommend you to use the ",(0,i.jsx)(t.code,{children:"UUID"})," type for the ",(0,i.jsx)(t.code,{children:"id"})," field. You can generate a new ",(0,i.jsx)(t.code,{children:"UUID"})," value by calling the ",(0,i.jsx)(t.code,{children:"UUID.generate()"})," method already provided by the framework."]})}),"\n",(0,i.jsx)(t.h2,{id:"entities-naming-convention",children:"Entities naming convention"}),"\n",(0,i.jsx)(t.p,{children:"Entities are a representation of your application state in a specific moment, so name them as closely to your domain objects as possible. Typical entity names are nouns that might appear when you think about your app. In an e-commerce application, some entities would be:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Cart"}),"\n",(0,i.jsx)(t.li,{children:"Product"}),"\n",(0,i.jsx)(t.li,{children:"UserProfile"}),"\n",(0,i.jsx)(t.li,{children:"Order"}),"\n",(0,i.jsx)(t.li,{children:"Address"}),"\n",(0,i.jsx)(t.li,{children:"PaymentMethod"}),"\n",(0,i.jsx)(t.li,{children:"Stock"}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Entities live within the entities directory of the project source: ",(0,i.jsx)(t.code,{children:"/src/entities"}),"."]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-text",children:"\n\u251c\u2500\u2500 src\n\u2502 \u251c\u2500\u2500 commands\n\u2502 \u251c\u2500\u2500 common\n\u2502 \u251c\u2500\u2500 config\n\u2502 \u251c\u2500\u2500 entities <------ put them here\n\u2502 \u251c\u2500\u2500 events\n\u2502 \u251c\u2500\u2500 index.ts\n\u2502 \u2514\u2500\u2500 read-models\n"})})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},5163:(e,t,n)=>{n.d(t,{Z:()=>s});n(7294);const i={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var r=n(5893);function s(e){let{children:t}=e;return(0,r.jsxs)("div",{className:i.terminalWindow,children:[(0,r.jsx)("div",{className:i.terminalWindowHeader,children:(0,r.jsxs)("div",{className:i.buttons,children:[(0,r.jsx)("span",{className:i.dot,style:{background:"#f25f58"}}),(0,r.jsx)("span",{className:i.dot,style:{background:"#fbbe3c"}}),(0,r.jsx)("span",{className:i.dot,style:{background:"#58cb42"}})]})}),(0,r.jsx)("div",{className:i.terminalWindowBody,children:t})]})}},5876:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/reducer-faf967cd976ea38d84e14551aa3af383.gif"},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>o});var i=n(7294);const r={},s=i.createContext(r);function o(e){const t=i.useContext(s);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/352d8b40.186f06fe.js b/assets/js/352d8b40.186f06fe.js new file mode 100644 index 000000000..25f35012d --- /dev/null +++ b/assets/js/352d8b40.186f06fe.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6502],{7041:(t,e,i)=>{i.r(e),i.d(e,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>r,metadata:()=>s,toc:()=>l});var n=i(5893),o=i(1151),a=i(5163);const r={description:"Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators."},c="Notifications",s={id:"architecture/notifications",title:"Notifications",description:"Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators.",source:"@site/docs/03_architecture/07_notifications.mdx",sourceDirName:"03_architecture",slug:"/architecture/notifications",permalink:"/architecture/notifications",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/07_notifications.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:7,frontMatter:{description:"Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators."},sidebar:"docs",previous:{title:"Read model",permalink:"/architecture/read-model"},next:{title:"Queries",permalink:"/architecture/queries"}},d={},l=[{value:"Declaring a notification",id:"declaring-a-notification",level:2},{value:"Separating by topic",id:"separating-by-topic",level:2},{value:"Separating by partition key",id:"separating-by-partition-key",level:2},{value:"Reacting to notifications",id:"reacting-to-notifications",level:2}];function h(t){const e={code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,o.a)(),...t.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(e.h1,{id:"notifications",children:"Notifications"}),"\n",(0,n.jsx)(e.p,{children:"Notifications are an important concept in event-driven architecture, and they play a crucial role in informing interested parties about certain events that take place within an application."}),"\n",(0,n.jsx)(e.h2,{id:"declaring-a-notification",children:"Declaring a notification"}),"\n",(0,n.jsxs)(e.p,{children:["In Booster, notifications are defined as classes decorated with the ",(0,n.jsx)(e.code,{children:"@Notification"})," decorator. Here's a minimal example to illustrate this:"]}),"\n",(0,n.jsx)(a.Z,{children:(0,n.jsx)(e.pre,{children:(0,n.jsx)(e.code,{className:"language-typescript",metastring:'title="src/notifications/cart-abandoned.ts"',children:"import { Notification } from '@boostercloud/framework-core'\n\n@Notification()\nexport class CartAbandoned {}\n"})})}),"\n",(0,n.jsxs)(e.p,{children:["As you can see, to define a notification you simply need to import the ",(0,n.jsx)(e.code,{children:"@Notification"})," decorator from the @boostercloud/framework-core library and use it to decorate a class. In this case, the class ",(0,n.jsx)(e.code,{children:"CartAbandoned"})," represents a notification that informs interested parties that a cart has been abandoned."]}),"\n",(0,n.jsx)(e.h2,{id:"separating-by-topic",children:"Separating by topic"}),"\n",(0,n.jsxs)(e.p,{children:["By default, all notifications in the application will be sent to the same topic called ",(0,n.jsx)(e.code,{children:"defaultTopic"}),". To configure this, you can specify a different topic name in the ",(0,n.jsx)(e.code,{children:"@Notification"})," decorator:"]}),"\n",(0,n.jsx)(a.Z,{children:(0,n.jsx)(e.pre,{children:(0,n.jsx)(e.code,{className:"language-typescript",metastring:'title="src/notifications/cart-abandoned-topic.ts"',children:"import { Notification } from '@boostercloud/framework-core'\n\n@Notification({ topic: 'cart-abandoned' })\nexport class CartAbandoned {}\n"})})}),"\n",(0,n.jsxs)(e.p,{children:["In this example, the ",(0,n.jsx)(e.code,{children:"CartAbandoned"})," notification will be sent to the ",(0,n.jsx)(e.code,{children:"cart-abandoned"})," topic, instead of the default topic."]}),"\n",(0,n.jsx)(e.h2,{id:"separating-by-partition-key",children:"Separating by partition key"}),"\n",(0,n.jsxs)(e.p,{children:["By default, all the notifications in the application will share a partition key called ",(0,n.jsx)(e.code,{children:"default"}),". This means that, by default, all the notifications in the application will be processed in order, which may not be as performant."]}),"\n",(0,n.jsx)(e.p,{children:"To change this, you can use the @partitionKey decorator to specify a field that will be used as a partition key for each notification:"}),"\n",(0,n.jsx)(a.Z,{children:(0,n.jsx)(e.pre,{children:(0,n.jsx)(e.code,{className:"language-typescript",metastring:'title="src/notifications/cart-abandoned-partition-key.ts"',children:"import { Notification, partitionKey } from '@boostercloud/framework-core'\n\n@Notification({ topic: 'cart-abandoned' })\nexport class CartAbandoned {\n public constructor(@partitionKey readonly key: string) {}\n}\n"})})}),"\n",(0,n.jsxs)(e.p,{children:["In this example, each ",(0,n.jsx)(e.code,{children:"CartAbandoned"})," notification will have its own partition key, which is specified in the constructor as the field ",(0,n.jsx)(e.code,{children:"key"}),", it can be called in any way you want. This will allow for parallel processing of notifications, making the system more performant."]}),"\n",(0,n.jsx)(e.h2,{id:"reacting-to-notifications",children:"Reacting to notifications"}),"\n",(0,n.jsx)(e.p,{children:"Just like events, notifications can be handled by event handlers in order to trigger other processes. Event handlers are responsible for listening to events and notifications, and then performing specific actions in response to them."}),"\n",(0,n.jsxs)(e.p,{children:["In conclusion, defining notifications in the Booster Framework is a simple and straightforward process that can be done using the ",(0,n.jsx)(e.code,{children:"@Notification"})," and ",(0,n.jsx)(e.code,{children:"@partitionKey"})," decorators."]})]})}function p(t={}){const{wrapper:e}={...(0,o.a)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(h,{...t})}):h(t)}},5163:(t,e,i)=>{i.d(e,{Z:()=>a});i(7294);const n={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var o=i(5893);function a(t){let{children:e}=t;return(0,o.jsxs)("div",{className:n.terminalWindow,children:[(0,o.jsx)("div",{className:n.terminalWindowHeader,children:(0,o.jsxs)("div",{className:n.buttons,children:[(0,o.jsx)("span",{className:n.dot,style:{background:"#f25f58"}}),(0,o.jsx)("span",{className:n.dot,style:{background:"#fbbe3c"}}),(0,o.jsx)("span",{className:n.dot,style:{background:"#58cb42"}})]})}),(0,o.jsx)("div",{className:n.terminalWindowBody,children:e})]})}},1151:(t,e,i)=>{i.d(e,{Z:()=>c,a:()=>r});var n=i(7294);const o={},a=n.createContext(o);function r(t){const e=n.useContext(a);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function c(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:r(t.components),n.createElement(a.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/352d8b40.f4a10177.js b/assets/js/352d8b40.f4a10177.js deleted file mode 100644 index c6f721b1d..000000000 --- a/assets/js/352d8b40.f4a10177.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6502],{7041:(t,e,i)=>{i.r(e),i.d(e,{assets:()=>d,contentTitle:()=>c,default:()=>p,frontMatter:()=>r,metadata:()=>s,toc:()=>l});var n=i(5893),o=i(1151),a=i(5163);const r={description:"Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators."},c="Notifications",s={id:"architecture/notifications",title:"Notifications",description:"Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators.",source:"@site/docs/03_architecture/07_notifications.mdx",sourceDirName:"03_architecture",slug:"/architecture/notifications",permalink:"/architecture/notifications",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/07_notifications.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:7,frontMatter:{description:"Documentation for defining notifications in the Booster Framework using the @Notification and @partitionKey decorators."},sidebar:"docs",previous:{title:"Read model",permalink:"/architecture/read-model"},next:{title:"Queries",permalink:"/architecture/queries"}},d={},l=[{value:"Declaring a notification",id:"declaring-a-notification",level:2},{value:"Separating by topic",id:"separating-by-topic",level:2},{value:"Separating by partition key",id:"separating-by-partition-key",level:2},{value:"Reacting to notifications",id:"reacting-to-notifications",level:2}];function h(t){const e={code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,o.a)(),...t.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(e.h1,{id:"notifications",children:"Notifications"}),"\n",(0,n.jsx)(e.p,{children:"Notifications are an important concept in event-driven architecture, and they play a crucial role in informing interested parties about certain events that take place within an application."}),"\n",(0,n.jsx)(e.h2,{id:"declaring-a-notification",children:"Declaring a notification"}),"\n",(0,n.jsxs)(e.p,{children:["In Booster, notifications are defined as classes decorated with the ",(0,n.jsx)(e.code,{children:"@Notification"})," decorator. Here's a minimal example to illustrate this:"]}),"\n",(0,n.jsx)(a.Z,{children:(0,n.jsx)(e.pre,{children:(0,n.jsx)(e.code,{className:"language-typescript",metastring:'title="src/notifications/cart-abandoned.ts"',children:"import { Notification } from '@boostercloud/framework-core'\n\n@Notification()\nexport class CartAbandoned {}\n"})})}),"\n",(0,n.jsxs)(e.p,{children:["As you can see, to define a notification you simply need to import the ",(0,n.jsx)(e.code,{children:"@Notification"})," decorator from the @boostercloud/framework-core library and use it to decorate a class. In this case, the class ",(0,n.jsx)(e.code,{children:"CartAbandoned"})," represents a notification that informs interested parties that a cart has been abandoned."]}),"\n",(0,n.jsx)(e.h2,{id:"separating-by-topic",children:"Separating by topic"}),"\n",(0,n.jsxs)(e.p,{children:["By default, all notifications in the application will be sent to the same topic called ",(0,n.jsx)(e.code,{children:"defaultTopic"}),". To configure this, you can specify a different topic name in the ",(0,n.jsx)(e.code,{children:"@Notification"})," decorator:"]}),"\n",(0,n.jsx)(a.Z,{children:(0,n.jsx)(e.pre,{children:(0,n.jsx)(e.code,{className:"language-typescript",metastring:'title="src/notifications/cart-abandoned-topic.ts"',children:"import { Notification } from '@boostercloud/framework-core'\n\n@Notification({ topic: 'cart-abandoned' })\nexport class CartAbandoned {}\n"})})}),"\n",(0,n.jsxs)(e.p,{children:["In this example, the ",(0,n.jsx)(e.code,{children:"CartAbandoned"})," notification will be sent to the ",(0,n.jsx)(e.code,{children:"cart-abandoned"})," topic, instead of the default topic."]}),"\n",(0,n.jsx)(e.h2,{id:"separating-by-partition-key",children:"Separating by partition key"}),"\n",(0,n.jsxs)(e.p,{children:["By default, all the notifications in the application will share a partition key called ",(0,n.jsx)(e.code,{children:"default"}),". This means that, by default, all the notifications in the application will be processed in order, which may not be as performant."]}),"\n",(0,n.jsx)(e.p,{children:"To change this, you can use the @partitionKey decorator to specify a field that will be used as a partition key for each notification:"}),"\n",(0,n.jsx)(a.Z,{children:(0,n.jsx)(e.pre,{children:(0,n.jsx)(e.code,{className:"language-typescript",metastring:'title="src/notifications/cart-abandoned-partition-key.ts"',children:"import { Notification, partitionKey } from '@boostercloud/framework-core'\n\n@Notification({ topic: 'cart-abandoned' })\nexport class CartAbandoned {\n public constructor(@partitionKey readonly key: string) {}\n}\n"})})}),"\n",(0,n.jsxs)(e.p,{children:["In this example, each ",(0,n.jsx)(e.code,{children:"CartAbandoned"})," notification will have its own partition key, which is specified in the constructor as the field ",(0,n.jsx)(e.code,{children:"key"}),", it can be called in any way you want. This will allow for parallel processing of notifications, making the system more performant."]}),"\n",(0,n.jsx)(e.h2,{id:"reacting-to-notifications",children:"Reacting to notifications"}),"\n",(0,n.jsx)(e.p,{children:"Just like events, notifications can be handled by event handlers in order to trigger other processes. Event handlers are responsible for listening to events and notifications, and then performing specific actions in response to them."}),"\n",(0,n.jsxs)(e.p,{children:["In conclusion, defining notifications in the Booster Framework is a simple and straightforward process that can be done using the ",(0,n.jsx)(e.code,{children:"@Notification"})," and ",(0,n.jsx)(e.code,{children:"@partitionKey"})," decorators."]})]})}function p(t={}){const{wrapper:e}={...(0,o.a)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(h,{...t})}):h(t)}},5163:(t,e,i)=>{i.d(e,{Z:()=>a});i(7294);const n={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var o=i(5893);function a(t){let{children:e}=t;return(0,o.jsxs)("div",{className:n.terminalWindow,children:[(0,o.jsx)("div",{className:n.terminalWindowHeader,children:(0,o.jsxs)("div",{className:n.buttons,children:[(0,o.jsx)("span",{className:n.dot,style:{background:"#f25f58"}}),(0,o.jsx)("span",{className:n.dot,style:{background:"#fbbe3c"}}),(0,o.jsx)("span",{className:n.dot,style:{background:"#58cb42"}})]})}),(0,o.jsx)("div",{className:n.terminalWindowBody,children:e})]})}},1151:(t,e,i)=>{i.d(e,{Z:()=>c,a:()=>r});var n=i(7294);const o={},a=n.createContext(o);function r(t){const e=n.useContext(a);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function c(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:r(t.components),n.createElement(a.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/3c6e0dde.028f3f7a.js b/assets/js/3c6e0dde.028f3f7a.js deleted file mode 100644 index b5a4f5f4d..000000000 --- a/assets/js/3c6e0dde.028f3f7a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4454],{4606:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>l,metadata:()=>d,toc:()=>h});var s=t(5893),o=t(1151),i=t(5163),r=t(2735);const l={description:"How to have the backend up and running for a blog application in a few minutes"},a="Build a Booster app in minutes",d={id:"getting-started/coding",title:"Build a Booster app in minutes",description:"How to have the backend up and running for a blog application in a few minutes",source:"@site/docs/02_getting-started/coding.mdx",sourceDirName:"02_getting-started",slug:"/getting-started/coding",permalink:"/getting-started/coding",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/02_getting-started/coding.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{description:"How to have the backend up and running for a blog application in a few minutes"},sidebar:"docs",previous:{title:"Installation",permalink:"/getting-started/installation"},next:{title:"Booster architecture",permalink:"/architecture/event-driven"}},c={},h=[{value:"1. Create the project",id:"1-create-the-project",level:3},{value:"2. First command",id:"2-first-command",level:3},{value:"3. First event",id:"3-first-event",level:3},{value:"4. First entity",id:"4-first-entity",level:3},{value:"5. First read model",id:"5-first-read-model",level:3},{value:"6. Deployment",id:"6-deployment",level:3},{value:"6.1 Running your application locally",id:"61-running-your-application-locally",level:4},{value:"6.2 Deploying to the cloud",id:"62-deploying-to-the-cloud",level:4},{value:"7. Testing",id:"7-testing",level:3},{value:"7.1 Creating posts",id:"71-creating-posts",level:4},{value:"7.2 Retrieving all posts",id:"72-retrieving-all-posts",level:4},{value:"7.3 Retrieving specific post",id:"73-retrieving-specific-post",level:4},{value:"8. Removing the stack",id:"8-removing-the-stack",level:3},{value:"9. More functionalities",id:"9-more-functionalities",level:3},{value:"Examples and walkthroughs",id:"examples-and-walkthroughs",level:2},{value:"Creation of a question-asking application backend",id:"creation-of-a-question-asking-application-backend",level:3},{value:"All the guides and examples",id:"all-the-guides-and-examples",level:3}];function p(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"build-a-booster-app-in-minutes",children:"Build a Booster app in minutes"}),"\n",(0,s.jsx)(n.p,{children:"In this section, we will go through all the necessary steps to have the backend up and\nrunning for a blog application in just a few minutes."}),"\n",(0,s.jsxs)(n.p,{children:["Before starting, make sure to ",(0,s.jsx)(n.a,{href:"/getting-started/installation",children:"have Booster CLI installed"}),". If you also want to deploy your application to your cloud provider, check out the ",(0,s.jsx)(n.a,{href:"../going-deeper/infrastructure-providers",children:"Provider configuration"})," section."]}),"\n",(0,s.jsx)(n.h3,{id:"1-create-the-project",children:"1. Create the project"}),"\n",(0,s.jsx)(n.p,{children:"First of all, we will use the Booster CLI tool generators to create a project."}),"\n",(0,s.jsxs)(n.p,{children:["In your favourite terminal, run this command ",(0,s.jsx)(n.code,{children:"boost new:project boosted-blog"})," and follow\nthe instructions. After some prompted questions, the CLI will ask you to select one of the available providers to set up as the main provider that will be used."]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"? What's the package name of your provider infrastructure library? (Use arrow keys)\n @boostercloud/framework-provider-azure (Azure)\n\u276f @boostercloud/framework-provider-aws (AWS) - Deprecated\n Other\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["When asked for the provider, select AWS as that is what we have\nconfigured ",(0,s.jsx)(n.a,{href:"../going-deeper/infrastructure-providers#aws-provider-setup",children:"here"})," for the example. You can use another provider if you want, or add more providers once you have created the project."]}),"\n",(0,s.jsx)(n.p,{children:"If you don't know what provider you are going to use, and you just want to execute your Booster application locally, you can select one and change it later!"}),"\n",(0,s.jsx)(n.p,{children:"After choosing your provider, you will see your project generated!:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"> boost new:project boosted-blog\n\n...\n\n\u2139 boost new \ud83d\udea7\n\u2714 Creating project root\n\u2714 Generating config files\n\u2714 Installing dependencies\n\u2139 Project generated!\n"})})}),"\n",(0,s.jsxs)(n.admonition,{type:"tip",children:[(0,s.jsxs)(n.p,{children:["If you prefer to create the project with default parameters, you can run the command as ",(0,s.jsx)(n.code,{children:"boost new:project booster-blog --default"}),". The default\nparameters are as follows:"]}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:'Project name: The one provided when running the command, in this case "booster-blog"'}),"\n",(0,s.jsx)(n.li,{children:"Provider: AWS"}),"\n",(0,s.jsx)(n.li,{children:'Description, author, homepage and repository: ""'}),"\n",(0,s.jsx)(n.li,{children:"License: MIT"}),"\n",(0,s.jsx)(n.li,{children:"Version: 0.1.0"}),"\n"]})]}),"\n",(0,s.jsxs)(n.p,{children:["In case you want to specify each parameter without following the instructions, you can use the following flags with this structure ",(0,s.jsx)(n.code,{children:"="}),"."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{style:{textAlign:"left"},children:"Flag"}),(0,s.jsx)(n.th,{style:{textAlign:"left"},children:"Short version"}),(0,s.jsx)(n.th,{style:{textAlign:"left"},children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--homepage"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-H"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"The website of this project"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--author"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-a"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"Author of this project"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--description"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-d"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"A short description"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--license"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-l"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"License used in this project"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--providerPackageName"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-p"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"Package name implementing the cloud provider integration where the application will be deployed"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--repository"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-r"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"The URL of the repository"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--version"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-v"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"The initial version"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["Additionally, you can use the ",(0,s.jsx)(n.code,{children:"--skipInstall"})," flag if you want to skip installing dependencies and the ",(0,s.jsx)(n.code,{children:"--skipGit"})," flag in case you want to skip git initialization."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["Booster CLI commands follow this structure: ",(0,s.jsx)(n.code,{children:"boost [] []"}),".\nLet's break down the command we have just executed:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"boost"})," is the Booster CLI executable"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"new:project"}),' is the "subcommand" part. In this case, it is composed of two parts separated by a colon. The first part, ',(0,s.jsx)(n.code,{children:"new"}),", means that we want to generate a new resource. The second part, ",(0,s.jsx)(n.code,{children:"project"}),", indicates which kind of resource we are interested in. Other examples are ",(0,s.jsx)(n.code,{children:"new:command"}),", ",(0,s.jsx)(n.code,{children:"new:event"}),", etc. We'll see a bunch of them later."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"boosted-blog"}),' is a "parameter" for the subcommand ',(0,s.jsx)(n.code,{children:"new:project"}),". Flags and parameters are optional and their meaning and shape depend on the subcommand you used. In this case, we are specifying the name of the project we are creating."]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["You can always use the ",(0,s.jsx)(n.code,{children:"--help"})," flag to get all the available options for each cli command."]})}),"\n",(0,s.jsxs)(n.p,{children:["When finished, you'll see some scaffolding that has been generated. The project name will be the\nproject's root so ",(0,s.jsx)(n.code,{children:"cd"})," into it:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"cd boosted-blog\n"})})}),"\n",(0,s.jsx)(n.p,{children:"There you should have these files and directories already generated:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u251c\u2500\u2500 .eslintignore\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 .eslintrc.js\n\u251c\u2500\u2500 .prettierrc.yaml\n\u251c\u2500\u2500 package-lock.json\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 src\n\u2502 \u251c\u2500\u2500 commands\n\u2502 \u251c\u2500\u2500 common\n\u2502 \u251c\u2500\u2500 config\n\u2502 \u2502 \u2514\u2500\u2500 config.ts\n\u2502 \u251c\u2500\u2500 entities\n\u2502 \u251c\u2500\u2500 events\n\u2502 \u251c\u2500\u2500 event-handlers\n\u2502 \u251c\u2500\u2500 read-models\n\u2502 \u2514\u2500\u2500 index.ts\n\u251c\u2500\u2500 tsconfig.eslint.json\n\u2514\u2500\u2500 tsconfig.json\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Now open the project in your favorite editor, e.g. ",(0,s.jsx)(n.a,{href:"https://code.visualstudio.com/",children:"Visual Studio Code"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"2-first-command",children:"2. First command"}),"\n",(0,s.jsxs)(n.p,{children:["Commands define the input to our system, so we'll start by generating our first\n",(0,s.jsx)(n.a,{href:"/architecture/command",children:"command"})," to create posts. Use the command generator, while in the project's root\ndirectory, as follows:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:command CreatePost --fields postId:UUID title:string content:string author:string\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"new:command"})," generator creates a ",(0,s.jsx)(n.code,{children:"create-post.ts"})," file in the ",(0,s.jsx)(n.code,{children:"commands"})," folder:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u2514\u2500\u2500 src\n \u2514\u2500\u2500 commands\n \u2514\u2500\u2500 create-post.ts\n"})}),"\n",(0,s.jsx)(n.p,{children:"As we mentioned before, commands are the input of our system. They're sent\nby the users of our application. When they are received you can validate its data,\nexecute some business logic, and register one or more events. Therefore, we have to define two more things:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Who is authorized to run this command."}),"\n",(0,s.jsx)(n.li,{children:"The events that it will trigger."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Booster allows you to define authorization strategies (we will cover that\nlater). Let's start by allowing anyone to send this command to our application.\nTo do that, open the file we have just generated and add the string ",(0,s.jsx)(n.code,{children:"'all'"})," to the\n",(0,s.jsx)(n.code,{children:"authorize"})," parameter of the ",(0,s.jsx)(n.code,{children:"@Command"})," decorator. Your ",(0,s.jsx)(n.code,{children:"CreatePost"})," command should look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@Command({\n authorize: 'all', // Specify authorized roles here. Use 'all' to authorize anyone\n})\nexport class CreatePost {\n public constructor(\n readonly postId: UUID,\n readonly title: string,\n readonly content: string,\n readonly author: string\n ) {}\n\n public static async handle(command: CreatePost, register: Register): Promise {\n register.events(/* YOUR EVENT HERE */)\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"3-first-event",children:"3. First event"}),"\n",(0,s.jsxs)(n.p,{children:["Instead of creating, updating, or deleting objects, Booster stores data in the form of events.\nThey are records of facts and represent the source of truth. Let's generate an event called ",(0,s.jsx)(n.code,{children:"PostCreated"}),"\nthat will contain the initial post info:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:event PostCreated --fields postId:UUID title:string content:string author:string\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"new:event"})," generator creates a new file under the ",(0,s.jsx)(n.code,{children:"src/events"})," directory.\nThe name of the file is the name of the event:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u2514\u2500\u2500 src\n \u2514\u2500\u2500 events\n \u2514\u2500\u2500 post-created.ts\n"})}),"\n",(0,s.jsxs)(n.p,{children:["All events in Booster must target an entity, so we need to implement an ",(0,s.jsx)(n.code,{children:"entityID"}),"\nmethod. From there, we'll return the identifier of the post created, the field\n",(0,s.jsx)(n.code,{children:"postID"}),". This identifier will be used later by Booster to build the final state\nof the ",(0,s.jsx)(n.code,{children:"Post"})," automatically. Edit the ",(0,s.jsx)(n.code,{children:"entityID"})," method in ",(0,s.jsx)(n.code,{children:"events/post-created.ts"}),"\nto return our ",(0,s.jsx)(n.code,{children:"postID"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/events/post-created.ts\n\n@Event\nexport class PostCreated {\n public constructor(\n readonly postId: UUID,\n readonly title: string,\n readonly content: string,\n readonly author: string\n ) {}\n\n public entityID(): UUID {\n return this.postId\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Now that we have an event, we can edit the ",(0,s.jsx)(n.code,{children:"CreatePost"})," command to emit it. Let's change\nthe command's ",(0,s.jsx)(n.code,{children:"handle"})," method to look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/commands/create-post.ts::handle\npublic static async handle(command: CreatePost, register: Register): Promise {\n register.events(new PostCreated(command.postId, command.title, command.content, command.author))\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"Remember to import the event class correctly on the top of the file:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { PostCreated } from '../events/post-created'\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We can do any validation in the command handler before storing the event, for our\nexample, we'll just save the received data in the ",(0,s.jsx)(n.code,{children:"PostCreated"})," event."]}),"\n",(0,s.jsx)(n.h3,{id:"4-first-entity",children:"4. First entity"}),"\n",(0,s.jsxs)(n.p,{children:["So far, our ",(0,s.jsx)(n.code,{children:"PostCreated"})," event suggests we need a ",(0,s.jsx)(n.code,{children:"Post"})," entity. Entities are a\nrepresentation of our system internal state. They are in charge of reducing (combining) all the events\nwith the same ",(0,s.jsx)(n.code,{children:"entityID"}),". Let's generate our ",(0,s.jsx)(n.code,{children:"Post"})," entity:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:entity Post --fields title:string content:string author:string --reduces PostCreated\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["You should see now a new file called ",(0,s.jsx)(n.code,{children:"post.ts"})," in the ",(0,s.jsx)(n.code,{children:"src/entities"})," directory."]}),"\n",(0,s.jsxs)(n.p,{children:["This time, besides using the ",(0,s.jsx)(n.code,{children:"--fields"})," flag, we use the ",(0,s.jsx)(n.code,{children:"--reduces"})," flag to specify the events the entity will reduce and, this way, produce the Post current state. The generator will create one ",(0,s.jsx)(n.em,{children:"reducer function"})," for each event we have specified (only one in this case)."]}),"\n",(0,s.jsxs)(n.p,{children:["Reducer functions in Booster work similarly to the ",(0,s.jsx)(n.code,{children:"reduce"})," callbacks in Javascript: they receive an event\nand the current state of the entity, and returns the next version of the same entity.\nIn this case, when we receive a ",(0,s.jsx)(n.code,{children:"PostCreated"})," event, we can just return a new ",(0,s.jsx)(n.code,{children:"Post"})," entity copying the fields\nfrom the event. There is no previous state of the Post as we are creating it for the first time:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/entities/post.ts\n@Entity\nexport class Post {\n public constructor(public id: UUID, readonly title: string, readonly content: string, readonly author: string) {}\n\n @Reduces(PostCreated)\n public static reducePostCreated(event: PostCreated, currentPost?: Post): Post {\n return new Post(event.postId, event.title, event.content, event.author)\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"Entities represent our domain model and can be queried from command or\nevent handlers to make business decisions or enforcing business rules."}),"\n",(0,s.jsx)(n.h3,{id:"5-first-read-model",children:"5. First read model"}),"\n",(0,s.jsxs)(n.p,{children:["In a real application, we rarely want to make public our entire domain model (entities)\nincluding all their fields. What is more, different users may have different views of the data depending\non their permissions or their use cases. That's the goal of ",(0,s.jsx)(n.code,{children:"ReadModels"}),". Client applications can query or\nsubscribe to them."]}),"\n",(0,s.jsxs)(n.p,{children:["Read models are ",(0,s.jsx)(n.em,{children:"projections"})," of one or more entities into a new object that is reachable through the query and subscriptions APIs. Let's generate a ",(0,s.jsx)(n.code,{children:"PostReadModel"})," that projects our\n",(0,s.jsx)(n.code,{children:"Post"})," entity:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:read-model PostReadModel --fields title:string author:string --projects Post:id\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["We have used a new flag, ",(0,s.jsx)(n.code,{children:"--projects"}),", that allow us to specify the entities (can be many) the read model will\nwatch for changes. You might be wondering what is the ",(0,s.jsx)(n.code,{children:":id"})," after the entity name. That's the ",(0,s.jsx)(n.a,{href:"/architecture/read-model#the-projection-function",children:"joinKey"}),",\nbut you can forget about it now."]}),"\n",(0,s.jsxs)(n.p,{children:["As you might guess, the read-model generator will create a file called\n",(0,s.jsx)(n.code,{children:"post-read-model.ts"})," under ",(0,s.jsx)(n.code,{children:"src/read-models"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u2514\u2500\u2500 src\n \u2514\u2500\u2500 read-models\n \u2514\u2500\u2500 post-read-model.ts\n"})}),"\n",(0,s.jsx)(n.p,{children:"There are two things to do when creating a read model:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Define who is authorized to query or subscribe it"}),"\n",(0,s.jsx)(n.li,{children:"Add the logic of the projection functions, where you can filter, combine, etc., the entities fields."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["While commands define the input to our system, read models define the output, and together they compound\nthe public API of a Booster application. Let's do the same we did in the command and authorize ",(0,s.jsx)(n.code,{children:"all"})," to\nquery/subscribe the ",(0,s.jsx)(n.code,{children:"PostReadModel"}),". Also, and for learning purposes, we will exclude the ",(0,s.jsx)(n.code,{children:"content"})," field\nfrom the ",(0,s.jsx)(n.code,{children:"Post"})," entity, so it won't be returned when users request the read model."]}),"\n",(0,s.jsxs)(n.p,{children:["Edit the ",(0,s.jsx)(n.code,{children:"post-read-model.ts"})," file to look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/read-models/post-read-model.ts\n@ReadModel({\n authorize: 'all', // Specify authorized roles here. Use 'all' to authorize anyone\n})\nexport class PostReadModel {\n public constructor(public id: UUID, readonly title: string, readonly author: string) {}\n\n @Projects(Post, 'id')\n public static projectPost(entity: Post, currentPostReadModel?: PostReadModel): ProjectionResult {\n return new PostReadModel(entity.id, entity.title, entity.author)\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"6-deployment",children:"6. Deployment"}),"\n",(0,s.jsx)(n.p,{children:"At this point, we've:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Created a publicly accessible command"}),"\n",(0,s.jsx)(n.li,{children:"Emitted an event as a mechanism to store data"}),"\n",(0,s.jsx)(n.li,{children:"Reduced the event into an entity to have a representation of our internal state"}),"\n",(0,s.jsx)(n.li,{children:"Projected the entity into a read model that is also publicly accessible."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"With this, you already know the basics to build event-driven, CQRS-based applications\nwith Booster."}),"\n",(0,s.jsx)(n.p,{children:"You can check that code compiles correctly by running the build command:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost build\n"})})}),"\n",(0,s.jsx)(n.p,{children:"You can also clean the compiled code by running:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost clean\n"})})}),"\n",(0,s.jsx)(n.h4,{id:"61-running-your-application-locally",children:"6.1 Running your application locally"}),"\n",(0,s.jsx)(n.p,{children:"Now, let's run our application to see it working. It is as simple as running:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost start -e local\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["This will execute a local ",(0,s.jsx)(n.code,{children:"Express.js"})," server and will try to expose it in port ",(0,s.jsx)(n.code,{children:"3000"}),". You can change the port by using the ",(0,s.jsx)(n.code,{children:"-p"})," option:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost start -e local -p 8080\n"})})}),"\n",(0,s.jsx)(n.h4,{id:"62-deploying-to-the-cloud",children:"6.2 Deploying to the cloud"}),"\n",(0,s.jsx)(n.p,{children:"Also, we can deploy our application to the cloud with no additional changes by running\nthe deploy command:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost deploy -e production\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["This is the Booster magic! \u2728 When running the start or the deploy commands, Booster will handle the creation of all the resources, ",(0,s.jsx)(n.em,{children:"like Lambdas, API Gateway,"}),' and the "glue" between them; ',(0,s.jsx)(n.em,{children:"permissions, events, triggers, etc."})," It even creates a fully functional GraphQL API!"]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsx)(n.p,{children:"Deploy command automatically builds the project for you before performing updates in the cloud provider, so, build command it's not required beforehand."})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["With ",(0,s.jsx)(n.code,{children:"-e production"})," we are specifying which environment we want to deploy. We'll talk about them later."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"If at this point you still don\u2019t believe everything is done, feel free to check in your provider\u2019s console. You should see, as in the AWS example below, that the stack and all the services are up and running! It will be the same for other providers. \ud83d\ude80"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"resources",src:t(2822).Z+"",width:"2726",height:"1276"})}),"\n",(0,s.jsxs)(n.p,{children:["When deploying, it will take a couple of minutes to deploy all the resources. Once finished, you will see\ninformation about your application endpoints and other outputs. For this example, we will\nonly need to pick the output ending in ",(0,s.jsx)(n.code,{children:"httpURL"}),", e.g.:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"https://.execute-api.us-east-1.amazonaws.com/production\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["By default, the full error stack trace is send to a local file, ",(0,s.jsx)(n.code,{children:"./errors.log"}),". To see the full error stack trace directly from the console, use the ",(0,s.jsx)(n.code,{children:"--verbose"})," flag."]})}),"\n",(0,s.jsx)(n.h3,{id:"7-testing",children:"7. Testing"}),"\n",(0,s.jsx)(n.p,{children:"Let's get started testing the project. We will perform three actions:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Add a couple of posts"}),"\n",(0,s.jsx)(n.li,{children:"Retrieve all posts"}),"\n",(0,s.jsx)(n.li,{children:"Retrieve a specific post"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Booster applications provide you with a GraphQL API out of the box. You send commands using\n",(0,s.jsx)(n.em,{children:"mutations"})," and get read models data using ",(0,s.jsx)(n.em,{children:"queries"})," or ",(0,s.jsx)(n.em,{children:"subscriptions"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["In this section, we will be sending requests by hand using the free ",(0,s.jsx)(n.a,{href:"https://altair.sirmuel.design/",children:"Altair"})," GraphQL client,\nwhich is very simple and straightforward for this guide. However, you can use any client you want. Your endpoint URL should look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"/graphql\n"})}),"\n",(0,s.jsx)(n.h4,{id:"71-creating-posts",children:"7.1 Creating posts"}),"\n",(0,s.jsxs)(n.p,{children:["Let's use two mutations to send two ",(0,s.jsx)(n.code,{children:"CreatePost"})," commands."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:'mutation {\n CreatePost(\n input: {\n postId: "95ddb544-4a60-439f-a0e4-c57e806f2f6e"\n title: "Build a blog in 10 minutes with Booster"\n content: "I am so excited to write my first post"\n author: "Boosted developer"\n }\n )\n}\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:'mutation {\n CreatePost(\n input: {\n postId: "05670e55-fd31-490e-b585-3a0096db0412"\n title: "Booster framework rocks"\n content: "I am so excited for writing the second post"\n author: "Another boosted developer"\n }\n )\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"The expected response for each of those requests should be:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "CreatePost": true\n }\n}\n'})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["In this example, the IDs are generated on the client-side. When running production applications consider adding validation for ID uniqueness. For this example, we have used ",(0,s.jsx)(n.a,{href:"https://www.uuidgenerator.net/version4",children:"a UUID generator"})]})}),"\n",(0,s.jsx)(n.h4,{id:"72-retrieving-all-posts",children:"7.2 Retrieving all posts"}),"\n",(0,s.jsxs)(n.p,{children:["Let's perform a GraphQL ",(0,s.jsx)(n.code,{children:"query"})," that will be hitting our ",(0,s.jsx)(n.code,{children:"PostReadModel"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:"query {\n PostReadModels {\n id\n title\n author\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"It should respond with something like:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "PostReadModels": [\n {\n "id": "05670e55-fd31-490e-b585-3a0096db0412",\n "title": "Booster framework rocks",\n "author": "Another boosted developer"\n },\n {\n "id": "95ddb544-4a60-439f-a0e4-c57e806f2f6e",\n "title": "Build a blog in 10 minutes with Booster",\n "author": "Boosted developer"\n }\n ]\n }\n}\n'})}),"\n",(0,s.jsx)(n.h4,{id:"73-retrieving-specific-post",children:"7.3 Retrieving specific post"}),"\n",(0,s.jsxs)(n.p,{children:["It is also possible to retrieve specific a ",(0,s.jsx)(n.code,{children:"Post"})," by adding the ",(0,s.jsx)(n.code,{children:"id"})," as input, e.g.:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:'query {\n PostReadModel(id: "95ddb544-4a60-439f-a0e4-c57e806f2f6e") {\n id\n title\n author\n }\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"You should get a response similar to this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "PostReadModel": {\n "id": "95ddb544-4a60-439f-a0e4-c57e806f2f6e",\n "title": "Build a blog in 10 minutes with Booster",\n "author": "Boosted developer"\n }\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"8-removing-the-stack",children:"8. Removing the stack"}),"\n",(0,s.jsxs)(n.p,{children:["It is convenient to destroy all the infrastructure created after you stop using\nit to avoid generating cloud resource costs. Execute the following command from\nthe root of the project. For safety reasons, you have to confirm this action by\nwriting the project's name, in our case ",(0,s.jsx)(n.code,{children:"boosted-blog"})," that is the same used when\nwe run ",(0,s.jsx)(n.code,{children:"new:project"})," CLI command."]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"> boost nuke -e production\n\n? Please, enter the app name to confirm deletion of all resources: boosted-blog\n"})})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Congratulations! You've built a serverless backend in less than 10 minutes. We hope you have enjoyed discovering the magic of the Booster Framework."}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"9-more-functionalities",children:"9. More functionalities"}),"\n",(0,s.jsx)(n.p,{children:"This is a really basic example of a Booster application. The are many other features Booster provides like:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Use a more complex authorization schema for commands and read models based on user roles"}),"\n",(0,s.jsx)(n.li,{children:"Use GraphQL subscriptions to get updates in real-time"}),"\n",(0,s.jsx)(n.li,{children:"Make events trigger other events"}),"\n",(0,s.jsx)(n.li,{children:"Deploy static content"}),"\n",(0,s.jsx)(n.li,{children:"Reading entities within command handlers to apply domain-driven decisions"}),"\n",(0,s.jsx)(n.li,{children:"And much more..."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Continue reading to dig more. You've just scratched the surface of all the Booster\ncapabilities!"}),"\n",(0,s.jsx)(n.h2,{id:"examples-and-walkthroughs",children:"Examples and walkthroughs"}),"\n",(0,s.jsx)(n.h3,{id:"creation-of-a-question-asking-application-backend",children:"Creation of a question-asking application backend"}),"\n",(0,s.jsxs)(n.p,{children:["In the following video, you will find how to create a backend for a question-asking application from scratch. This application would allow\nusers to create questions and like them. This video goes from creating the project to incrementally deploying features in the application.\nYou can find the code both for the frontend and the backend in ",(0,s.jsx)(r.do,{children:(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/examples/tree/master/askme",children:"this GitHub repo"})}),"."]}),"\n",(0,s.jsx)("div",{align:"center",children:(0,s.jsx)("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/C4K2M-orT8k",title:"YouTube video player",frameBorder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowFullScreen:!0})}),"\n",(0,s.jsx)(n.h3,{id:"all-the-guides-and-examples",children:"All the guides and examples"}),"\n",(0,s.jsxs)(n.p,{children:["Check out the ",(0,s.jsx)(r.dM,{children:(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/examples",children:"example apps repository"})})," to see Booster in use."]})]})}function u(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},2735:(e,n,t)=>{t.d(n,{do:()=>a,dM:()=>l,Dh:()=>d});var s=t(7294),o=t(719),i=t(5893);const r=e=>{let{href:n,onClick:t,children:s}=e;return(0,i.jsx)("a",{href:n,target:"_blank",rel:"noopener noreferrer",onClick:e=>{t&&t()},children:s})},l=e=>{let{children:n}=e;return c(n,"YY7T3ZSZ")},a=e=>{let{children:n}=e;return c(n,"NE1EADCK")},d=e=>{let{children:n}=e;return c(n,"AXTW7ICE")};function c(e,n){const{text:t,href:l}=function(e){if(s.isValidElement(e)&&e.props.href)return{text:e.props.children,href:e.props.href};return{text:"",href:""}}(e);return(0,i.jsx)(r,{href:l,onClick:()=>o.R.startAndTrackEvent(n),children:t})}},5163:(e,n,t)=>{t.d(n,{Z:()=>i});t(7294);const s={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var o=t(5893);function i(e){let{children:n}=e;return(0,o.jsxs)("div",{className:s.terminalWindow,children:[(0,o.jsx)("div",{className:s.terminalWindowHeader,children:(0,o.jsxs)("div",{className:s.buttons,children:[(0,o.jsx)("span",{className:s.dot,style:{background:"#f25f58"}}),(0,o.jsx)("span",{className:s.dot,style:{background:"#fbbe3c"}}),(0,o.jsx)("span",{className:s.dot,style:{background:"#58cb42"}})]})}),(0,o.jsx)("div",{className:s.terminalWindowBody,children:n})]})}},2822:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/aws-resources-e620ed48140a022aae2ca68d0c52b496.png"},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>r});var s=t(7294);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3c6e0dde.67ae457c.js b/assets/js/3c6e0dde.67ae457c.js new file mode 100644 index 000000000..2f94496f5 --- /dev/null +++ b/assets/js/3c6e0dde.67ae457c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4454],{4606:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>l,metadata:()=>d,toc:()=>h});var s=t(5893),o=t(1151),i=t(5163),r=t(2735);const l={description:"How to have the backend up and running for a blog application in a few minutes"},a="Build a Booster app in minutes",d={id:"getting-started/coding",title:"Build a Booster app in minutes",description:"How to have the backend up and running for a blog application in a few minutes",source:"@site/docs/02_getting-started/coding.mdx",sourceDirName:"02_getting-started",slug:"/getting-started/coding",permalink:"/getting-started/coding",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/02_getting-started/coding.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{description:"How to have the backend up and running for a blog application in a few minutes"},sidebar:"docs",previous:{title:"Installation",permalink:"/getting-started/installation"},next:{title:"Booster architecture",permalink:"/architecture/event-driven"}},c={},h=[{value:"1. Create the project",id:"1-create-the-project",level:3},{value:"2. First command",id:"2-first-command",level:3},{value:"3. First event",id:"3-first-event",level:3},{value:"4. First entity",id:"4-first-entity",level:3},{value:"5. First read model",id:"5-first-read-model",level:3},{value:"6. Deployment",id:"6-deployment",level:3},{value:"6.1 Running your application locally",id:"61-running-your-application-locally",level:4},{value:"6.2 Deploying to the cloud",id:"62-deploying-to-the-cloud",level:4},{value:"7. Testing",id:"7-testing",level:3},{value:"7.1 Creating posts",id:"71-creating-posts",level:4},{value:"7.2 Retrieving all posts",id:"72-retrieving-all-posts",level:4},{value:"7.3 Retrieving specific post",id:"73-retrieving-specific-post",level:4},{value:"8. Removing the stack",id:"8-removing-the-stack",level:3},{value:"9. More functionalities",id:"9-more-functionalities",level:3},{value:"Examples and walkthroughs",id:"examples-and-walkthroughs",level:2},{value:"Creation of a question-asking application backend",id:"creation-of-a-question-asking-application-backend",level:3},{value:"All the guides and examples",id:"all-the-guides-and-examples",level:3}];function p(e){const n={a:"a",admonition:"admonition",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"build-a-booster-app-in-minutes",children:"Build a Booster app in minutes"}),"\n",(0,s.jsx)(n.p,{children:"In this section, we will go through all the necessary steps to have the backend up and\nrunning for a blog application in just a few minutes."}),"\n",(0,s.jsxs)(n.p,{children:["Before starting, make sure to ",(0,s.jsx)(n.a,{href:"/getting-started/installation",children:"have Booster CLI installed"}),". If you also want to deploy your application to your cloud provider, check out the ",(0,s.jsx)(n.a,{href:"../going-deeper/infrastructure-providers",children:"Provider configuration"})," section."]}),"\n",(0,s.jsx)(n.h3,{id:"1-create-the-project",children:"1. Create the project"}),"\n",(0,s.jsx)(n.p,{children:"First of all, we will use the Booster CLI tool generators to create a project."}),"\n",(0,s.jsxs)(n.p,{children:["In your favourite terminal, run this command ",(0,s.jsx)(n.code,{children:"boost new:project boosted-blog"})," and follow\nthe instructions. After some prompted questions, the CLI will ask you to select one of the available providers to set up as the main provider that will be used."]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"? What's the package name of your provider infrastructure library? (Use arrow keys)\n @boostercloud/framework-provider-azure (Azure)\n\u276f @boostercloud/framework-provider-aws (AWS) - Deprecated\n Other\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["When asked for the provider, select AWS as that is what we have\nconfigured ",(0,s.jsx)(n.a,{href:"../going-deeper/infrastructure-providers#aws-provider-setup",children:"here"})," for the example. You can use another provider if you want, or add more providers once you have created the project."]}),"\n",(0,s.jsx)(n.p,{children:"If you don't know what provider you are going to use, and you just want to execute your Booster application locally, you can select one and change it later!"}),"\n",(0,s.jsx)(n.p,{children:"After choosing your provider, you will see your project generated!:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"> boost new:project boosted-blog\n\n...\n\n\u2139 boost new \ud83d\udea7\n\u2714 Creating project root\n\u2714 Generating config files\n\u2714 Installing dependencies\n\u2139 Project generated!\n"})})}),"\n",(0,s.jsxs)(n.admonition,{type:"tip",children:[(0,s.jsxs)(n.p,{children:["If you prefer to create the project with default parameters, you can run the command as ",(0,s.jsx)(n.code,{children:"boost new:project booster-blog --default"}),". The default\nparameters are as follows:"]}),(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:'Project name: The one provided when running the command, in this case "booster-blog"'}),"\n",(0,s.jsx)(n.li,{children:"Provider: AWS"}),"\n",(0,s.jsx)(n.li,{children:'Description, author, homepage and repository: ""'}),"\n",(0,s.jsx)(n.li,{children:"License: MIT"}),"\n",(0,s.jsx)(n.li,{children:"Version: 0.1.0"}),"\n"]})]}),"\n",(0,s.jsxs)(n.p,{children:["In case you want to specify each parameter without following the instructions, you can use the following flags with this structure ",(0,s.jsx)(n.code,{children:"="}),"."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{style:{textAlign:"left"},children:"Flag"}),(0,s.jsx)(n.th,{style:{textAlign:"left"},children:"Short version"}),(0,s.jsx)(n.th,{style:{textAlign:"left"},children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--homepage"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-H"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"The website of this project"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--author"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-a"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"Author of this project"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--description"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-d"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"A short description"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--license"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-l"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"License used in this project"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--providerPackageName"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-p"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"Package name implementing the cloud provider integration where the application will be deployed"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--repository"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-r"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"The URL of the repository"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"--version"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:(0,s.jsx)(n.code,{children:"-v"})}),(0,s.jsx)(n.td,{style:{textAlign:"left"},children:"The initial version"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["Additionally, you can use the ",(0,s.jsx)(n.code,{children:"--skipInstall"})," flag if you want to skip installing dependencies and the ",(0,s.jsx)(n.code,{children:"--skipGit"})," flag in case you want to skip git initialization."]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["Booster CLI commands follow this structure: ",(0,s.jsx)(n.code,{children:"boost [] []"}),".\nLet's break down the command we have just executed:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"boost"})," is the Booster CLI executable"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"new:project"}),' is the "subcommand" part. In this case, it is composed of two parts separated by a colon. The first part, ',(0,s.jsx)(n.code,{children:"new"}),", means that we want to generate a new resource. The second part, ",(0,s.jsx)(n.code,{children:"project"}),", indicates which kind of resource we are interested in. Other examples are ",(0,s.jsx)(n.code,{children:"new:command"}),", ",(0,s.jsx)(n.code,{children:"new:event"}),", etc. We'll see a bunch of them later."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"boosted-blog"}),' is a "parameter" for the subcommand ',(0,s.jsx)(n.code,{children:"new:project"}),". Flags and parameters are optional and their meaning and shape depend on the subcommand you used. In this case, we are specifying the name of the project we are creating."]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["You can always use the ",(0,s.jsx)(n.code,{children:"--help"})," flag to get all the available options for each cli command."]})}),"\n",(0,s.jsxs)(n.p,{children:["When finished, you'll see some scaffolding that has been generated. The project name will be the\nproject's root so ",(0,s.jsx)(n.code,{children:"cd"})," into it:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-shell",children:"cd boosted-blog\n"})})}),"\n",(0,s.jsx)(n.p,{children:"There you should have these files and directories already generated:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u251c\u2500\u2500 .eslintignore\n\u251c\u2500\u2500 .gitignore\n\u251c\u2500\u2500 .eslintrc.js\n\u251c\u2500\u2500 .prettierrc.yaml\n\u251c\u2500\u2500 package-lock.json\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 src\n\u2502 \u251c\u2500\u2500 commands\n\u2502 \u251c\u2500\u2500 common\n\u2502 \u251c\u2500\u2500 config\n\u2502 \u2502 \u2514\u2500\u2500 config.ts\n\u2502 \u251c\u2500\u2500 entities\n\u2502 \u251c\u2500\u2500 events\n\u2502 \u251c\u2500\u2500 event-handlers\n\u2502 \u251c\u2500\u2500 read-models\n\u2502 \u2514\u2500\u2500 index.ts\n\u251c\u2500\u2500 tsconfig.eslint.json\n\u2514\u2500\u2500 tsconfig.json\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Now open the project in your favorite editor, e.g. ",(0,s.jsx)(n.a,{href:"https://code.visualstudio.com/",children:"Visual Studio Code"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"2-first-command",children:"2. First command"}),"\n",(0,s.jsxs)(n.p,{children:["Commands define the input to our system, so we'll start by generating our first\n",(0,s.jsx)(n.a,{href:"/architecture/command",children:"command"})," to create posts. Use the command generator, while in the project's root\ndirectory, as follows:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:command CreatePost --fields postId:UUID title:string content:string author:string\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"new:command"})," generator creates a ",(0,s.jsx)(n.code,{children:"create-post.ts"})," file in the ",(0,s.jsx)(n.code,{children:"commands"})," folder:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u2514\u2500\u2500 src\n \u2514\u2500\u2500 commands\n \u2514\u2500\u2500 create-post.ts\n"})}),"\n",(0,s.jsx)(n.p,{children:"As we mentioned before, commands are the input of our system. They're sent\nby the users of our application. When they are received you can validate its data,\nexecute some business logic, and register one or more events. Therefore, we have to define two more things:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Who is authorized to run this command."}),"\n",(0,s.jsx)(n.li,{children:"The events that it will trigger."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Booster allows you to define authorization strategies (we will cover that\nlater). Let's start by allowing anyone to send this command to our application.\nTo do that, open the file we have just generated and add the string ",(0,s.jsx)(n.code,{children:"'all'"})," to the\n",(0,s.jsx)(n.code,{children:"authorize"})," parameter of the ",(0,s.jsx)(n.code,{children:"@Command"})," decorator. Your ",(0,s.jsx)(n.code,{children:"CreatePost"})," command should look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@Command({\n authorize: 'all', // Specify authorized roles here. Use 'all' to authorize anyone\n})\nexport class CreatePost {\n public constructor(\n readonly postId: UUID,\n readonly title: string,\n readonly content: string,\n readonly author: string\n ) {}\n\n public static async handle(command: CreatePost, register: Register): Promise {\n register.events(/* YOUR EVENT HERE */)\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"3-first-event",children:"3. First event"}),"\n",(0,s.jsxs)(n.p,{children:["Instead of creating, updating, or deleting objects, Booster stores data in the form of events.\nThey are records of facts and represent the source of truth. Let's generate an event called ",(0,s.jsx)(n.code,{children:"PostCreated"}),"\nthat will contain the initial post info:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:event PostCreated --fields postId:UUID title:string content:string author:string\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"new:event"})," generator creates a new file under the ",(0,s.jsx)(n.code,{children:"src/events"})," directory.\nThe name of the file is the name of the event:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u2514\u2500\u2500 src\n \u2514\u2500\u2500 events\n \u2514\u2500\u2500 post-created.ts\n"})}),"\n",(0,s.jsxs)(n.p,{children:["All events in Booster must target an entity, so we need to implement an ",(0,s.jsx)(n.code,{children:"entityID"}),"\nmethod. From there, we'll return the identifier of the post created, the field\n",(0,s.jsx)(n.code,{children:"postID"}),". This identifier will be used later by Booster to build the final state\nof the ",(0,s.jsx)(n.code,{children:"Post"})," automatically. Edit the ",(0,s.jsx)(n.code,{children:"entityID"})," method in ",(0,s.jsx)(n.code,{children:"events/post-created.ts"}),"\nto return our ",(0,s.jsx)(n.code,{children:"postID"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/events/post-created.ts\n\n@Event\nexport class PostCreated {\n public constructor(\n readonly postId: UUID,\n readonly title: string,\n readonly content: string,\n readonly author: string\n ) {}\n\n public entityID(): UUID {\n return this.postId\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Now that we have an event, we can edit the ",(0,s.jsx)(n.code,{children:"CreatePost"})," command to emit it. Let's change\nthe command's ",(0,s.jsx)(n.code,{children:"handle"})," method to look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/commands/create-post.ts::handle\npublic static async handle(command: CreatePost, register: Register): Promise {\n register.events(new PostCreated(command.postId, command.title, command.content, command.author))\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"Remember to import the event class correctly on the top of the file:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import { PostCreated } from '../events/post-created'\n"})}),"\n",(0,s.jsxs)(n.p,{children:["We can do any validation in the command handler before storing the event, for our\nexample, we'll just save the received data in the ",(0,s.jsx)(n.code,{children:"PostCreated"})," event."]}),"\n",(0,s.jsx)(n.h3,{id:"4-first-entity",children:"4. First entity"}),"\n",(0,s.jsxs)(n.p,{children:["So far, our ",(0,s.jsx)(n.code,{children:"PostCreated"})," event suggests we need a ",(0,s.jsx)(n.code,{children:"Post"})," entity. Entities are a\nrepresentation of our system internal state. They are in charge of reducing (combining) all the events\nwith the same ",(0,s.jsx)(n.code,{children:"entityID"}),". Let's generate our ",(0,s.jsx)(n.code,{children:"Post"})," entity:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:entity Post --fields title:string content:string author:string --reduces PostCreated\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["You should see now a new file called ",(0,s.jsx)(n.code,{children:"post.ts"})," in the ",(0,s.jsx)(n.code,{children:"src/entities"})," directory."]}),"\n",(0,s.jsxs)(n.p,{children:["This time, besides using the ",(0,s.jsx)(n.code,{children:"--fields"})," flag, we use the ",(0,s.jsx)(n.code,{children:"--reduces"})," flag to specify the events the entity will reduce and, this way, produce the Post current state. The generator will create one ",(0,s.jsx)(n.em,{children:"reducer function"})," for each event we have specified (only one in this case)."]}),"\n",(0,s.jsxs)(n.p,{children:["Reducer functions in Booster work similarly to the ",(0,s.jsx)(n.code,{children:"reduce"})," callbacks in Javascript: they receive an event\nand the current state of the entity, and returns the next version of the same entity.\nIn this case, when we receive a ",(0,s.jsx)(n.code,{children:"PostCreated"})," event, we can just return a new ",(0,s.jsx)(n.code,{children:"Post"})," entity copying the fields\nfrom the event. There is no previous state of the Post as we are creating it for the first time:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/entities/post.ts\n@Entity\nexport class Post {\n public constructor(public id: UUID, readonly title: string, readonly content: string, readonly author: string) {}\n\n @Reduces(PostCreated)\n public static reducePostCreated(event: PostCreated, currentPost?: Post): Post {\n return new Post(event.postId, event.title, event.content, event.author)\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"Entities represent our domain model and can be queried from command or\nevent handlers to make business decisions or enforcing business rules."}),"\n",(0,s.jsx)(n.h3,{id:"5-first-read-model",children:"5. First read model"}),"\n",(0,s.jsxs)(n.p,{children:["In a real application, we rarely want to make public our entire domain model (entities)\nincluding all their fields. What is more, different users may have different views of the data depending\non their permissions or their use cases. That's the goal of ",(0,s.jsx)(n.code,{children:"ReadModels"}),". Client applications can query or\nsubscribe to them."]}),"\n",(0,s.jsxs)(n.p,{children:["Read models are ",(0,s.jsx)(n.em,{children:"projections"})," of one or more entities into a new object that is reachable through the query and subscriptions APIs. Let's generate a ",(0,s.jsx)(n.code,{children:"PostReadModel"})," that projects our\n",(0,s.jsx)(n.code,{children:"Post"})," entity:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost new:read-model PostReadModel --fields title:string author:string --projects Post:id\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["We have used a new flag, ",(0,s.jsx)(n.code,{children:"--projects"}),", that allow us to specify the entities (can be many) the read model will\nwatch for changes. You might be wondering what is the ",(0,s.jsx)(n.code,{children:":id"})," after the entity name. That's the ",(0,s.jsx)(n.a,{href:"/architecture/read-model#the-projection-function",children:"joinKey"}),",\nbut you can forget about it now."]}),"\n",(0,s.jsxs)(n.p,{children:["As you might guess, the read-model generator will create a file called\n",(0,s.jsx)(n.code,{children:"post-read-model.ts"})," under ",(0,s.jsx)(n.code,{children:"src/read-models"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"boosted-blog\n\u2514\u2500\u2500 src\n \u2514\u2500\u2500 read-models\n \u2514\u2500\u2500 post-read-model.ts\n"})}),"\n",(0,s.jsx)(n.p,{children:"There are two things to do when creating a read model:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Define who is authorized to query or subscribe it"}),"\n",(0,s.jsx)(n.li,{children:"Add the logic of the projection functions, where you can filter, combine, etc., the entities fields."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["While commands define the input to our system, read models define the output, and together they compound\nthe public API of a Booster application. Let's do the same we did in the command and authorize ",(0,s.jsx)(n.code,{children:"all"})," to\nquery/subscribe the ",(0,s.jsx)(n.code,{children:"PostReadModel"}),". Also, and for learning purposes, we will exclude the ",(0,s.jsx)(n.code,{children:"content"})," field\nfrom the ",(0,s.jsx)(n.code,{children:"Post"})," entity, so it won't be returned when users request the read model."]}),"\n",(0,s.jsxs)(n.p,{children:["Edit the ",(0,s.jsx)(n.code,{children:"post-read-model.ts"})," file to look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"// src/read-models/post-read-model.ts\n@ReadModel({\n authorize: 'all', // Specify authorized roles here. Use 'all' to authorize anyone\n})\nexport class PostReadModel {\n public constructor(public id: UUID, readonly title: string, readonly author: string) {}\n\n @Projects(Post, 'id')\n public static projectPost(entity: Post, currentPostReadModel?: PostReadModel): ProjectionResult {\n return new PostReadModel(entity.id, entity.title, entity.author)\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"6-deployment",children:"6. Deployment"}),"\n",(0,s.jsx)(n.p,{children:"At this point, we've:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Created a publicly accessible command"}),"\n",(0,s.jsx)(n.li,{children:"Emitted an event as a mechanism to store data"}),"\n",(0,s.jsx)(n.li,{children:"Reduced the event into an entity to have a representation of our internal state"}),"\n",(0,s.jsx)(n.li,{children:"Projected the entity into a read model that is also publicly accessible."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"With this, you already know the basics to build event-driven, CQRS-based applications\nwith Booster."}),"\n",(0,s.jsx)(n.p,{children:"You can check that code compiles correctly by running the build command:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost build\n"})})}),"\n",(0,s.jsx)(n.p,{children:"You can also clean the compiled code by running:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost clean\n"})})}),"\n",(0,s.jsx)(n.h4,{id:"61-running-your-application-locally",children:"6.1 Running your application locally"}),"\n",(0,s.jsx)(n.p,{children:"Now, let's run our application to see it working. It is as simple as running:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost start -e local\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["This will execute a local ",(0,s.jsx)(n.code,{children:"Express.js"})," server and will try to expose it in port ",(0,s.jsx)(n.code,{children:"3000"}),". You can change the port by using the ",(0,s.jsx)(n.code,{children:"-p"})," option:"]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost start -e local -p 8080\n"})})}),"\n",(0,s.jsx)(n.h4,{id:"62-deploying-to-the-cloud",children:"6.2 Deploying to the cloud"}),"\n",(0,s.jsx)(n.p,{children:"Also, we can deploy our application to the cloud with no additional changes by running\nthe deploy command:"}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"boost deploy -e production\n"})})}),"\n",(0,s.jsxs)(n.p,{children:["This is the Booster magic! \u2728 When running the start or the deploy commands, Booster will handle the creation of all the resources, ",(0,s.jsx)(n.em,{children:"like Lambdas, API Gateway,"}),' and the "glue" between them; ',(0,s.jsx)(n.em,{children:"permissions, events, triggers, etc."})," It even creates a fully functional GraphQL API!"]}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsx)(n.p,{children:"Deploy command automatically builds the project for you before performing updates in the cloud provider, so, build command it's not required beforehand."})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:["With ",(0,s.jsx)(n.code,{children:"-e production"})," we are specifying which environment we want to deploy. We'll talk about them later."]}),"\n"]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsx)(n.p,{children:"If at this point you still don\u2019t believe everything is done, feel free to check in your provider\u2019s console. You should see, as in the AWS example below, that the stack and all the services are up and running! It will be the same for other providers. \ud83d\ude80"})}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.img,{alt:"resources",src:t(2822).Z+"",width:"2726",height:"1276"})}),"\n",(0,s.jsxs)(n.p,{children:["When deploying, it will take a couple of minutes to deploy all the resources. Once finished, you will see\ninformation about your application endpoints and other outputs. For this example, we will\nonly need to pick the output ending in ",(0,s.jsx)(n.code,{children:"httpURL"}),", e.g.:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"https://.execute-api.us-east-1.amazonaws.com/production\n"})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["By default, the full error stack trace is send to a local file, ",(0,s.jsx)(n.code,{children:"./errors.log"}),". To see the full error stack trace directly from the console, use the ",(0,s.jsx)(n.code,{children:"--verbose"})," flag."]})}),"\n",(0,s.jsx)(n.h3,{id:"7-testing",children:"7. Testing"}),"\n",(0,s.jsx)(n.p,{children:"Let's get started testing the project. We will perform three actions:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Add a couple of posts"}),"\n",(0,s.jsx)(n.li,{children:"Retrieve all posts"}),"\n",(0,s.jsx)(n.li,{children:"Retrieve a specific post"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Booster applications provide you with a GraphQL API out of the box. You send commands using\n",(0,s.jsx)(n.em,{children:"mutations"})," and get read models data using ",(0,s.jsx)(n.em,{children:"queries"})," or ",(0,s.jsx)(n.em,{children:"subscriptions"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["In this section, we will be sending requests by hand using the free ",(0,s.jsx)(n.a,{href:"https://altair.sirmuel.design/",children:"Altair"})," GraphQL client,\nwhich is very simple and straightforward for this guide. However, you can use any client you want. Your endpoint URL should look like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"/graphql\n"})}),"\n",(0,s.jsx)(n.h4,{id:"71-creating-posts",children:"7.1 Creating posts"}),"\n",(0,s.jsxs)(n.p,{children:["Let's use two mutations to send two ",(0,s.jsx)(n.code,{children:"CreatePost"})," commands."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:'mutation {\n CreatePost(\n input: {\n postId: "95ddb544-4a60-439f-a0e4-c57e806f2f6e"\n title: "Build a blog in 10 minutes with Booster"\n content: "I am so excited to write my first post"\n author: "Boosted developer"\n }\n )\n}\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:'mutation {\n CreatePost(\n input: {\n postId: "05670e55-fd31-490e-b585-3a0096db0412"\n title: "Booster framework rocks"\n content: "I am so excited for writing the second post"\n author: "Another boosted developer"\n }\n )\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"The expected response for each of those requests should be:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "CreatePost": true\n }\n}\n'})}),"\n",(0,s.jsx)(n.admonition,{type:"note",children:(0,s.jsxs)(n.p,{children:["In this example, the IDs are generated on the client-side. When running production applications consider adding validation for ID uniqueness. For this example, we have used ",(0,s.jsx)(n.a,{href:"https://www.uuidgenerator.net/version4",children:"a UUID generator"})]})}),"\n",(0,s.jsx)(n.h4,{id:"72-retrieving-all-posts",children:"7.2 Retrieving all posts"}),"\n",(0,s.jsxs)(n.p,{children:["Let's perform a GraphQL ",(0,s.jsx)(n.code,{children:"query"})," that will be hitting our ",(0,s.jsx)(n.code,{children:"PostReadModel"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:"query {\n PostReadModels {\n id\n title\n author\n }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"It should respond with something like:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "PostReadModels": [\n {\n "id": "05670e55-fd31-490e-b585-3a0096db0412",\n "title": "Booster framework rocks",\n "author": "Another boosted developer"\n },\n {\n "id": "95ddb544-4a60-439f-a0e4-c57e806f2f6e",\n "title": "Build a blog in 10 minutes with Booster",\n "author": "Boosted developer"\n }\n ]\n }\n}\n'})}),"\n",(0,s.jsx)(n.h4,{id:"73-retrieving-specific-post",children:"7.3 Retrieving specific post"}),"\n",(0,s.jsxs)(n.p,{children:["It is also possible to retrieve specific a ",(0,s.jsx)(n.code,{children:"Post"})," by adding the ",(0,s.jsx)(n.code,{children:"id"})," as input, e.g.:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-graphql",children:'query {\n PostReadModel(id: "95ddb544-4a60-439f-a0e4-c57e806f2f6e") {\n id\n title\n author\n }\n}\n'})}),"\n",(0,s.jsx)(n.p,{children:"You should get a response similar to this:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "data": {\n "PostReadModel": {\n "id": "95ddb544-4a60-439f-a0e4-c57e806f2f6e",\n "title": "Build a blog in 10 minutes with Booster",\n "author": "Boosted developer"\n }\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"8-removing-the-stack",children:"8. Removing the stack"}),"\n",(0,s.jsxs)(n.p,{children:["It is convenient to destroy all the infrastructure created after you stop using\nit to avoid generating cloud resource costs. Execute the following command from\nthe root of the project. For safety reasons, you have to confirm this action by\nwriting the project's name, in our case ",(0,s.jsx)(n.code,{children:"boosted-blog"})," that is the same used when\nwe run ",(0,s.jsx)(n.code,{children:"new:project"})," CLI command."]}),"\n",(0,s.jsx)(i.Z,{children:(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"> boost nuke -e production\n\n? Please, enter the app name to confirm deletion of all resources: boosted-blog\n"})})}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Congratulations! You've built a serverless backend in less than 10 minutes. We hope you have enjoyed discovering the magic of the Booster Framework."}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"9-more-functionalities",children:"9. More functionalities"}),"\n",(0,s.jsx)(n.p,{children:"This is a really basic example of a Booster application. The are many other features Booster provides like:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Use a more complex authorization schema for commands and read models based on user roles"}),"\n",(0,s.jsx)(n.li,{children:"Use GraphQL subscriptions to get updates in real-time"}),"\n",(0,s.jsx)(n.li,{children:"Make events trigger other events"}),"\n",(0,s.jsx)(n.li,{children:"Deploy static content"}),"\n",(0,s.jsx)(n.li,{children:"Reading entities within command handlers to apply domain-driven decisions"}),"\n",(0,s.jsx)(n.li,{children:"And much more..."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Continue reading to dig more. You've just scratched the surface of all the Booster\ncapabilities!"}),"\n",(0,s.jsx)(n.h2,{id:"examples-and-walkthroughs",children:"Examples and walkthroughs"}),"\n",(0,s.jsx)(n.h3,{id:"creation-of-a-question-asking-application-backend",children:"Creation of a question-asking application backend"}),"\n",(0,s.jsxs)(n.p,{children:["In the following video, you will find how to create a backend for a question-asking application from scratch. This application would allow\nusers to create questions and like them. This video goes from creating the project to incrementally deploying features in the application.\nYou can find the code both for the frontend and the backend in ",(0,s.jsx)(r.do,{children:(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/examples/tree/master/askme",children:"this GitHub repo"})}),"."]}),"\n",(0,s.jsx)("div",{align:"center",children:(0,s.jsx)("iframe",{width:"560",height:"315",src:"https://www.youtube.com/embed/C4K2M-orT8k",title:"YouTube video player",frameBorder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowFullScreen:!0})}),"\n",(0,s.jsx)(n.h3,{id:"all-the-guides-and-examples",children:"All the guides and examples"}),"\n",(0,s.jsxs)(n.p,{children:["Check out the ",(0,s.jsx)(r.dM,{children:(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/examples",children:"example apps repository"})})," to see Booster in use."]})]})}function u(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(p,{...e})}):p(e)}},2735:(e,n,t)=>{t.d(n,{do:()=>a,dM:()=>l,Dh:()=>d});var s=t(7294),o=t(719),i=t(5893);const r=e=>{let{href:n,onClick:t,children:s}=e;return(0,i.jsx)("a",{href:n,target:"_blank",rel:"noopener noreferrer",onClick:e=>{t&&t()},children:s})},l=e=>{let{children:n}=e;return c(n,"YY7T3ZSZ")},a=e=>{let{children:n}=e;return c(n,"NE1EADCK")},d=e=>{let{children:n}=e;return c(n,"AXTW7ICE")};function c(e,n){const{text:t,href:l}=function(e){if(s.isValidElement(e)&&e.props.href)return{text:e.props.children,href:e.props.href};return{text:"",href:""}}(e);return(0,i.jsx)(r,{href:l,onClick:()=>o.R.startAndTrackEvent(n),children:t})}},5163:(e,n,t)=>{t.d(n,{Z:()=>i});t(7294);const s={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var o=t(5893);function i(e){let{children:n}=e;return(0,o.jsxs)("div",{className:s.terminalWindow,children:[(0,o.jsx)("div",{className:s.terminalWindowHeader,children:(0,o.jsxs)("div",{className:s.buttons,children:[(0,o.jsx)("span",{className:s.dot,style:{background:"#f25f58"}}),(0,o.jsx)("span",{className:s.dot,style:{background:"#fbbe3c"}}),(0,o.jsx)("span",{className:s.dot,style:{background:"#58cb42"}})]})}),(0,o.jsx)("div",{className:s.terminalWindowBody,children:n})]})}},2822:(e,n,t)=>{t.d(n,{Z:()=>s});const s=t.p+"assets/images/aws-resources-e620ed48140a022aae2ca68d0c52b496.png"},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>r});var s=t(7294);const o={},i=s.createContext(o);function r(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3f8caafb.215e0515.js b/assets/js/3f8caafb.215e0515.js deleted file mode 100644 index 5449c918a..000000000 --- a/assets/js/3f8caafb.215e0515.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4296],{9930:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>d,frontMatter:()=>i,metadata:()=>r,toc:()=>h});var s=t(5893),o=t(1151);const i={description:"Learn how to get Booster health information"},l=void 0,r={id:"going-deeper/health/sensor-health",title:"sensor-health",description:"Learn how to get Booster health information",source:"@site/docs/10_going-deeper/health/sensor-health.md",sourceDirName:"10_going-deeper/health",slug:"/going-deeper/health/sensor-health",permalink:"/going-deeper/health/sensor-health",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/health/sensor-health.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{description:"Learn how to get Booster health information"},sidebar:"docs",previous:{title:"Sensor",permalink:"/going-deeper/sensor"},next:{title:"Testing",permalink:"/going-deeper/testing"}},a={},h=[{value:"Health",id:"health",level:2},{value:"Supported Providers",id:"supported-providers",level:2},{value:"Enabling Health Functionality",id:"enabling-health-functionality",level:3},{value:"Health Endpoint",id:"health-endpoint",level:3},{value:"Available endpoints",id:"available-endpoints",level:4},{value:"Health Status Response",id:"health-status-response",level:3},{value:"Get specific component health information",id:"get-specific-component-health-information",level:3},{value:"Health configuration",id:"health-configuration",level:3},{value:"Booster components default configuration",id:"booster-components-default-configuration",level:4},{value:"User components configuration",id:"user-components-configuration",level:4},{value:"Create your own health endpoint",id:"create-your-own-health-endpoint",level:3},{value:"Booster health endpoints",id:"booster-health-endpoints",level:3},{value:"booster",id:"booster",level:4},{value:"booster/function",id:"boosterfunction",level:4},{value:"booster/database",id:"boosterdatabase",level:4},{value:"booster/database/events",id:"boosterdatabaseevents",level:4},{value:"booster/database/readmodels",id:"boosterdatabasereadmodels",level:4},{value:"Health status",id:"health-status",level:3},{value:"Securing health endpoints",id:"securing-health-endpoints",level:3},{value:"Example",id:"example",level:3}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h2,{id:"health",children:"Health"}),"\n",(0,s.jsx)(n.p,{children:"The Health functionality allows users to easily monitor the health status of their applications. With this functionality, users can make GET requests to a specific endpoint and retrieve detailed information about the health and status of their application components."}),"\n",(0,s.jsx)(n.h2,{id:"supported-providers",children:"Supported Providers"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Azure Provider"}),"\n",(0,s.jsx)(n.li,{children:"Local Provider"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"enabling-health-functionality",children:"Enabling Health Functionality"}),"\n",(0,s.jsx)(n.p,{children:"To enable the Health functionality in your Booster application, follow these steps:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Install or update to the latest version of the Booster framework, ensuring compatibility with the Health functionality."}),"\n",(0,s.jsx)(n.li,{children:"Enable the Booster Health endpoints in your application's configuration file. Example configuration in config.ts:"}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"Booster.configure('local', (config: BoosterConfig): void => {\n config.appName = 'my-store'\n config.providerPackage = '@boostercloud/framework-provider-local'\n Object.values(config.sensorConfiguration.health.booster).forEach((indicator) => {\n indicator.enabled = true\n })\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"Or enable only the components you want:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"Booster.configure('local', (config: BoosterConfig): void => {\n config.appName = 'my-store'\n config.providerPackage = '@boostercloud/framework-provider-local'\n const sensors = config.sensorConfiguration.health.booster\n sensors[BOOSTER_HEALTH_INDICATORS_IDS.DATABASE].enabled = true\n})\n"})}),"\n",(0,s.jsxs)(n.ol,{start:"3",children:["\n",(0,s.jsx)(n.li,{children:"Optionally, implement health checks for your application components. Each component should provide a health method that performs the appropriate checks and returns a response indicating the health status. Example:"}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import {\n BoosterConfig,\n HealthIndicatorResult,\n HealthIndicatorMetadata,\n HealthStatus,\n} from '@boostercloud/framework-types'\nimport { HealthSensor } from '@boostercloud/framework-core'\n\n@HealthSensor({\n id: 'application',\n name: 'my-application',\n enabled: true,\n details: true,\n showChildren: true,\n})\nexport class ApplicationHealthIndicator {\n public async health(\n config: BoosterConfig,\n healthIndicatorMetadata: HealthIndicatorMetadata\n ): Promise {\n return {\n status: HealthStatus.UP,\n } as HealthIndicatorResult\n }\n}\n"})}),"\n",(0,s.jsxs)(n.ol,{start:"4",children:["\n",(0,s.jsx)(n.li,{children:"A health check typically involves verifying the connectivity and status of the component, running any necessary tests, and returning an appropriate status code."}),"\n",(0,s.jsxs)(n.li,{children:["Start or restart your Booster application. The Health functionality will be available at the ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})," endpoint URL."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"health-endpoint",children:"Health Endpoint"}),"\n",(0,s.jsxs)(n.p,{children:["The Health functionality provides a dedicated endpoint where users can make GET requests to retrieve the health status of their application. The endpoint URL is: ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})]}),"\n",(0,s.jsxs)(n.p,{children:["This endpoint will return all the enabled Booster and application components health status. To get specific component health status, add the component status to the url. For example, to get the events status use: ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database/events",children:"https://your-application-url/sensor/health/booster/database/events"})]}),"\n",(0,s.jsx)(n.h4,{id:"available-endpoints",children:"Available endpoints"}),"\n",(0,s.jsx)(n.p,{children:"Booster provides the following endpoints to retrieve the enabled components:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"}),": All the components status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster",children:"https://your-application-url/sensor/health/booster"}),": Booster status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database",children:"https://your-application-url/sensor/health/booster/database"}),": Database status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database/events",children:"https://your-application-url/sensor/health/booster/database/events"}),": Events status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database/readmodels",children:"https://your-application-url/sensor/health/booster/database/readmodels"}),": ReadModels status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/function",children:"https://your-application-url/sensor/health/booster/function"}),": Functions status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/your-component-id",children:"https://your-application-url/sensor/health/your-component-id"}),": User defined status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/your-component-id/your-component-child-id",children:"https://your-application-url/sensor/health/your-component-id/your-component-child-id"}),": User child component status"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Depending on the ",(0,s.jsx)(n.code,{children:"showChildren"})," configuration, children components will be included or not."]}),"\n",(0,s.jsx)(n.h3,{id:"health-status-response",children:"Health Status Response"}),"\n",(0,s.jsx)(n.p,{children:"Each component response will contain the following information:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: The component or subsystem status"}),"\n",(0,s.jsx)(n.li,{children:"name: component description"}),"\n",(0,s.jsx)(n.li,{children:"id: string. unique component identifier. You can request a component status using the id in the url"}),"\n",(0,s.jsxs)(n.li,{children:["details: optional object. If ",(0,s.jsx)(n.code,{children:"details"})," is true, specific details about this component."]}),"\n",(0,s.jsxs)(n.li,{children:["components: optional object. If ",(0,s.jsx)(n.code,{children:"showChildren"})," is true, children components health status."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'[\n {\n "status": "UP",\n "details": {\n "urls": [\n "dbs/my-store-app"\n ]\n },\n "name": "Booster Database",\n "id": "booster/database",\n "components": [\n {\n "status": "UP",\n "details": {\n "url": "dbs/my-store-app/colls/my-store-app-events-store",\n "count": 6\n },\n "name": "Booster Database Events",\n "id": "booster/database/events"\n },\n {\n "status": "UP",\n "details": [\n {\n "url": "dbs/my-store-app/colls/my-store-app-ProductReadModel",\n "count": 1\n }\n ],\n "name": "Booster Database ReadModels",\n "id": "booster/database/readmodels"\n }\n ]\n }\n]\n'})}),"\n",(0,s.jsx)(n.h3,{id:"get-specific-component-health-information",children:"Get specific component health information"}),"\n",(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"id"})," field to get specific component health information. Booster provides the following ids:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"booster"}),"\n",(0,s.jsx)(n.li,{children:"booster/function"}),"\n",(0,s.jsx)(n.li,{children:"booster/database"}),"\n",(0,s.jsx)(n.li,{children:"booster/database/events"}),"\n",(0,s.jsx)(n.li,{children:"booster/database/readmodels"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"You can provide new components:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: 'application',\n})\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: 'application/child',\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"Add your own components to Booster:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: `${BOOSTER_HEALTH_INDICATORS_IDS.DATABASE}/extra`,\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"Or override Booster existing components with your own implementation:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: BOOSTER_HEALTH_INDICATORS_IDS.DATABASE,\n})\n"})}),"\n",(0,s.jsx)(n.h3,{id:"health-configuration",children:"Health configuration"}),"\n",(0,s.jsx)(n.p,{children:"Health components are fully configurable, allowing you to display the information you want at any moment."}),"\n",(0,s.jsx)(n.p,{children:"Configuration options:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"enabled: If false, this indicator and the components of this indicator will be skipped"}),"\n",(0,s.jsx)(n.li,{children:"details: If false, the indicator will not include the details"}),"\n",(0,s.jsxs)(n.li,{children:["showChildren: If false, this indicator will not include children components in the tree.","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Children components will be shown through children urls"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["authorize: Authorize configuration. ",(0,s.jsx)(n.a,{href:"https://docs.boosterframework.com/security/security",children:"See security documentation"})]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"booster-components-default-configuration",children:"Booster components default configuration"}),"\n",(0,s.jsx)(n.p,{children:"Booster sets the following default configuration for its own components:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"enabled: false"}),"\n",(0,s.jsx)(n.li,{children:"details: true"}),"\n",(0,s.jsx)(n.li,{children:"showChildren: true"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Change this configuration using the ",(0,s.jsx)(n.code,{children:"config.sensorConfiguration"})," object. This object provides:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.globalAuthorizer: Allow to define authorization configuration"}),"\n",(0,s.jsxs)(n.li,{children:["config.sensorConfiguration.health.booster: Allow to override default Booster components configuration","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.booster[BOOSTER_COMPONENT_ID].enabled"}),"\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.booster[BOOSTER_COMPONENT_ID].details"}),"\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.booster[BOOSTER_COMPONENT_ID].showChildren"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"user-components-configuration",children:"User components configuration"}),"\n",(0,s.jsxs)(n.p,{children:["Use ",(0,s.jsx)(n.code,{children:"@HealthSensor"})," parameters to configure user components. Example:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: 'user',\n name: 'my-application',\n enabled: true,\n details: true,\n showChildren: true,\n})\n"})}),"\n",(0,s.jsx)(n.h3,{id:"create-your-own-health-endpoint",children:"Create your own health endpoint"}),"\n",(0,s.jsxs)(n.p,{children:["Create your own health endpoint with a class annotated with ",(0,s.jsx)(n.code,{children:"@HealthSensor"})," decorator. This class\nshould define a ",(0,s.jsx)(n.code,{children:"health"})," method that returns a ",(0,s.jsx)(n.strong,{children:"HealthIndicatorResult"}),". Example:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import {\n BoosterConfig,\n HealthIndicatorResult,\n HealthIndicatorMetadata,\n HealthStatus,\n} from '@boostercloud/framework-types'\nimport { HealthSensor } from '@boostercloud/framework-core'\n\n@HealthSensor({\n id: 'application',\n name: 'my-application',\n enabled: true,\n details: true,\n showChildren: true,\n})\nexport class ApplicationHealthIndicator {\n public async health(\n config: BoosterConfig,\n healthIndicatorMetadata: HealthIndicatorMetadata\n ): Promise {\n return {\n status: HealthStatus.UP,\n } as HealthIndicatorResult\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"booster-health-endpoints",children:"Booster health endpoints"}),"\n",(0,s.jsx)(n.h4,{id:"booster",children:"booster"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if graphql function is UP and events are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"boosterVersion: Booster version number"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterfunction",children:"booster/function"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if graphql function is UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"graphQL_url: GraphQL function url"}),"\n",(0,s.jsxs)(n.li,{children:["cpus: Information about each logical CPU core.","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["cpu:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"model: Cpu model. Example: AMD EPYC 7763 64-Core Processor"}),"\n",(0,s.jsx)(n.li,{children:"speed: cpu speed in MHz"}),"\n",(0,s.jsxs)(n.li,{children:["times: The number of milliseconds the CPU/core spent in (see iostat)","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"user: CPU utilization that occurred while executing at the user level (application)"}),"\n",(0,s.jsx)(n.li,{children:"nice: CPU utilization that occurred while executing at the user level with nice priority."}),"\n",(0,s.jsx)(n.li,{children:"sys: CPU utilization that occurred while executing at the system level (kernel)."}),"\n",(0,s.jsx)(n.li,{children:"idle: CPU or CPUs were idle and the system did not have an outstanding disk I/O request."}),"\n",(0,s.jsx)(n.li,{children:"irq: CPU load system"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.li,{children:"timesPercentages: For each times value, the percentage over the total times"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["memory:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"totalBytes: the total amount of system memory in bytes as an integer."}),"\n",(0,s.jsx)(n.li,{children:"freeBytes: the amount of free system memory in bytes as an integer."}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterdatabase",children:"booster/database"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if events are UP and Read Models are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"urls: Database urls"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterdatabaseevents",children:"booster/database/events"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if events are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"AZURE PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"url: Events url"}),"\n",(0,s.jsx)(n.li,{children:"count: number of rows"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"LOCAL PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"file: event database file"}),"\n",(0,s.jsx)(n.li,{children:"count: number of rows"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterdatabasereadmodels",children:"booster/database/readmodels"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if Read Models are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"AZURE PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["For each Read Model:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"url: Event url"}),"\n",(0,s.jsx)(n.li,{children:"count: number of rows"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"LOCAL PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"file: Read Models database file"}),"\n",(0,s.jsx)(n.li,{children:"count: number of total rows"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Note"}),": details will be included only if ",(0,s.jsx)(n.code,{children:"details"})," is enabled"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"health-status",children:"Health status"}),"\n",(0,s.jsx)(n.p,{children:"Available status are"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"UP: The component or subsystem is working as expected"}),"\n",(0,s.jsx)(n.li,{children:"DOWN: The component is not working"}),"\n",(0,s.jsx)(n.li,{children:"OUT_OF_SERVICE: The component is out of service temporarily"}),"\n",(0,s.jsx)(n.li,{children:"UNKNOWN: The component state is unknown"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If a component throw an exception the status will be DOWN"}),"\n",(0,s.jsx)(n.h3,{id:"securing-health-endpoints",children:"Securing health endpoints"}),"\n",(0,s.jsxs)(n.p,{children:["To configure the health endpoints authorization use ",(0,s.jsx)(n.code,{children:"config.sensorConfiguration.health.globalAuthorizer"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"config.sensorConfiguration.health.globalAuthorizer = {\n authorize: 'all',\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the authorization process fails, the health endpoint will return a 401 error code"}),"\n",(0,s.jsx)(n.h3,{id:"example",children:"Example"}),"\n",(0,s.jsx)(n.p,{children:"If all components are enable and showChildren is set to true:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["A Request to ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})," will return:"]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 booster\n\u2502\xa0\xa0\u251c\u2500\u2500 database\n\u2502\xa0\xa0\xa0\xa0\u251c\u2500\u2500 events\n\u2502\xa0\xa0\xa0\xa0\u2514\u2500\u2500 readmodels\n\u2514\xa0\xa0\u2514\u2500\u2500 function\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the database component is disabled, the same url will return:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 booster\n\u2514\xa0\xa0\u2514\u2500\u2500 function\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If the request url is ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database",children:"https://your-application-url/sensor/health/database"}),", the component will not be returned"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"[Empty]\n"})}),"\n",(0,s.jsxs)(n.p,{children:["And the children components will be disabled too using direct url ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database/events",children:"https://your-application-url/sensor/health/database/events"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"[Empty]\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If database is enabled and showChildren is set to false and using ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 booster\n\u2502\xa0\xa0\u251c\u2500\u2500 database\n\u2502\xa0\xa0\u2514\u2500\u2500 function\n"})}),"\n",(0,s.jsxs)(n.p,{children:["using ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database",children:"https://your-application-url/sensor/health/database"}),", children will not be visible"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u2514\u2500\u2500 database\n"})}),"\n",(0,s.jsxs)(n.p,{children:["but you can access to them using the component url ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database/events",children:"https://your-application-url/sensor/health/database/events"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u2514\u2500\u2500 events\n"})})]})}function d(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>r,a:()=>l});var s=t(7294);const o={},i=s.createContext(o);function l(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:l(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/3f8caafb.e81611f2.js b/assets/js/3f8caafb.e81611f2.js new file mode 100644 index 000000000..894ab70dc --- /dev/null +++ b/assets/js/3f8caafb.e81611f2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[4296],{9930:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>d,frontMatter:()=>i,metadata:()=>r,toc:()=>h});var s=t(5893),o=t(1151);const i={description:"Learn how to get Booster health information"},l=void 0,r={id:"going-deeper/health/sensor-health",title:"sensor-health",description:"Learn how to get Booster health information",source:"@site/docs/10_going-deeper/health/sensor-health.md",sourceDirName:"10_going-deeper/health",slug:"/going-deeper/health/sensor-health",permalink:"/going-deeper/health/sensor-health",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/health/sensor-health.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{description:"Learn how to get Booster health information"},sidebar:"docs",previous:{title:"Sensor",permalink:"/going-deeper/sensor"},next:{title:"Testing",permalink:"/going-deeper/testing"}},a={},h=[{value:"Health",id:"health",level:2},{value:"Supported Providers",id:"supported-providers",level:2},{value:"Enabling Health Functionality",id:"enabling-health-functionality",level:3},{value:"Health Endpoint",id:"health-endpoint",level:3},{value:"Available endpoints",id:"available-endpoints",level:4},{value:"Health Status Response",id:"health-status-response",level:3},{value:"Get specific component health information",id:"get-specific-component-health-information",level:3},{value:"Health configuration",id:"health-configuration",level:3},{value:"Booster components default configuration",id:"booster-components-default-configuration",level:4},{value:"User components configuration",id:"user-components-configuration",level:4},{value:"Create your own health endpoint",id:"create-your-own-health-endpoint",level:3},{value:"Booster health endpoints",id:"booster-health-endpoints",level:3},{value:"booster",id:"booster",level:4},{value:"booster/function",id:"boosterfunction",level:4},{value:"booster/database",id:"boosterdatabase",level:4},{value:"booster/database/events",id:"boosterdatabaseevents",level:4},{value:"booster/database/readmodels",id:"boosterdatabasereadmodels",level:4},{value:"Health status",id:"health-status",level:3},{value:"Securing health endpoints",id:"securing-health-endpoints",level:3},{value:"Example",id:"example",level:3}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",h4:"h4",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h2,{id:"health",children:"Health"}),"\n",(0,s.jsx)(n.p,{children:"The Health functionality allows users to easily monitor the health status of their applications. With this functionality, users can make GET requests to a specific endpoint and retrieve detailed information about the health and status of their application components."}),"\n",(0,s.jsx)(n.h2,{id:"supported-providers",children:"Supported Providers"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Azure Provider"}),"\n",(0,s.jsx)(n.li,{children:"Local Provider"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"enabling-health-functionality",children:"Enabling Health Functionality"}),"\n",(0,s.jsx)(n.p,{children:"To enable the Health functionality in your Booster application, follow these steps:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Install or update to the latest version of the Booster framework, ensuring compatibility with the Health functionality."}),"\n",(0,s.jsx)(n.li,{children:"Enable the Booster Health endpoints in your application's configuration file. Example configuration in config.ts:"}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"Booster.configure('local', (config: BoosterConfig): void => {\n config.appName = 'my-store'\n config.providerPackage = '@boostercloud/framework-provider-local'\n Object.values(config.sensorConfiguration.health.booster).forEach((indicator) => {\n indicator.enabled = true\n })\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"Or enable only the components you want:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"Booster.configure('local', (config: BoosterConfig): void => {\n config.appName = 'my-store'\n config.providerPackage = '@boostercloud/framework-provider-local'\n const sensors = config.sensorConfiguration.health.booster\n sensors[BOOSTER_HEALTH_INDICATORS_IDS.DATABASE].enabled = true\n})\n"})}),"\n",(0,s.jsxs)(n.ol,{start:"3",children:["\n",(0,s.jsx)(n.li,{children:"Optionally, implement health checks for your application components. Each component should provide a health method that performs the appropriate checks and returns a response indicating the health status. Example:"}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import {\n BoosterConfig,\n HealthIndicatorResult,\n HealthIndicatorMetadata,\n HealthStatus,\n} from '@boostercloud/framework-types'\nimport { HealthSensor } from '@boostercloud/framework-core'\n\n@HealthSensor({\n id: 'application',\n name: 'my-application',\n enabled: true,\n details: true,\n showChildren: true,\n})\nexport class ApplicationHealthIndicator {\n public async health(\n config: BoosterConfig,\n healthIndicatorMetadata: HealthIndicatorMetadata\n ): Promise {\n return {\n status: HealthStatus.UP,\n } as HealthIndicatorResult\n }\n}\n"})}),"\n",(0,s.jsxs)(n.ol,{start:"4",children:["\n",(0,s.jsx)(n.li,{children:"A health check typically involves verifying the connectivity and status of the component, running any necessary tests, and returning an appropriate status code."}),"\n",(0,s.jsxs)(n.li,{children:["Start or restart your Booster application. The Health functionality will be available at the ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})," endpoint URL."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"health-endpoint",children:"Health Endpoint"}),"\n",(0,s.jsxs)(n.p,{children:["The Health functionality provides a dedicated endpoint where users can make GET requests to retrieve the health status of their application. The endpoint URL is: ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})]}),"\n",(0,s.jsxs)(n.p,{children:["This endpoint will return all the enabled Booster and application components health status. To get specific component health status, add the component status to the url. For example, to get the events status use: ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database/events",children:"https://your-application-url/sensor/health/booster/database/events"})]}),"\n",(0,s.jsx)(n.h4,{id:"available-endpoints",children:"Available endpoints"}),"\n",(0,s.jsx)(n.p,{children:"Booster provides the following endpoints to retrieve the enabled components:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"}),": All the components status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster",children:"https://your-application-url/sensor/health/booster"}),": Booster status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database",children:"https://your-application-url/sensor/health/booster/database"}),": Database status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database/events",children:"https://your-application-url/sensor/health/booster/database/events"}),": Events status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/database/readmodels",children:"https://your-application-url/sensor/health/booster/database/readmodels"}),": ReadModels status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/booster/function",children:"https://your-application-url/sensor/health/booster/function"}),": Functions status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/your-component-id",children:"https://your-application-url/sensor/health/your-component-id"}),": User defined status"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/your-component-id/your-component-child-id",children:"https://your-application-url/sensor/health/your-component-id/your-component-child-id"}),": User child component status"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Depending on the ",(0,s.jsx)(n.code,{children:"showChildren"})," configuration, children components will be included or not."]}),"\n",(0,s.jsx)(n.h3,{id:"health-status-response",children:"Health Status Response"}),"\n",(0,s.jsx)(n.p,{children:"Each component response will contain the following information:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: The component or subsystem status"}),"\n",(0,s.jsx)(n.li,{children:"name: component description"}),"\n",(0,s.jsx)(n.li,{children:"id: string. unique component identifier. You can request a component status using the id in the url"}),"\n",(0,s.jsxs)(n.li,{children:["details: optional object. If ",(0,s.jsx)(n.code,{children:"details"})," is true, specific details about this component."]}),"\n",(0,s.jsxs)(n.li,{children:["components: optional object. If ",(0,s.jsx)(n.code,{children:"showChildren"})," is true, children components health status."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'[\n {\n "status": "UP",\n "details": {\n "urls": [\n "dbs/my-store-app"\n ]\n },\n "name": "Booster Database",\n "id": "booster/database",\n "components": [\n {\n "status": "UP",\n "details": {\n "url": "dbs/my-store-app/colls/my-store-app-events-store",\n "count": 6\n },\n "name": "Booster Database Events",\n "id": "booster/database/events"\n },\n {\n "status": "UP",\n "details": [\n {\n "url": "dbs/my-store-app/colls/my-store-app-ProductReadModel",\n "count": 1\n }\n ],\n "name": "Booster Database ReadModels",\n "id": "booster/database/readmodels"\n }\n ]\n }\n]\n'})}),"\n",(0,s.jsx)(n.h3,{id:"get-specific-component-health-information",children:"Get specific component health information"}),"\n",(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"id"})," field to get specific component health information. Booster provides the following ids:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"booster"}),"\n",(0,s.jsx)(n.li,{children:"booster/function"}),"\n",(0,s.jsx)(n.li,{children:"booster/database"}),"\n",(0,s.jsx)(n.li,{children:"booster/database/events"}),"\n",(0,s.jsx)(n.li,{children:"booster/database/readmodels"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"You can provide new components:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: 'application',\n})\n"})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: 'application/child',\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"Add your own components to Booster:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: `${BOOSTER_HEALTH_INDICATORS_IDS.DATABASE}/extra`,\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"Or override Booster existing components with your own implementation:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: BOOSTER_HEALTH_INDICATORS_IDS.DATABASE,\n})\n"})}),"\n",(0,s.jsx)(n.h3,{id:"health-configuration",children:"Health configuration"}),"\n",(0,s.jsx)(n.p,{children:"Health components are fully configurable, allowing you to display the information you want at any moment."}),"\n",(0,s.jsx)(n.p,{children:"Configuration options:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"enabled: If false, this indicator and the components of this indicator will be skipped"}),"\n",(0,s.jsx)(n.li,{children:"details: If false, the indicator will not include the details"}),"\n",(0,s.jsxs)(n.li,{children:["showChildren: If false, this indicator will not include children components in the tree.","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Children components will be shown through children urls"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["authorize: Authorize configuration. ",(0,s.jsx)(n.a,{href:"https://docs.boosterframework.com/security/security",children:"See security documentation"})]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"booster-components-default-configuration",children:"Booster components default configuration"}),"\n",(0,s.jsx)(n.p,{children:"Booster sets the following default configuration for its own components:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"enabled: false"}),"\n",(0,s.jsx)(n.li,{children:"details: true"}),"\n",(0,s.jsx)(n.li,{children:"showChildren: true"}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Change this configuration using the ",(0,s.jsx)(n.code,{children:"config.sensorConfiguration"})," object. This object provides:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.globalAuthorizer: Allow to define authorization configuration"}),"\n",(0,s.jsxs)(n.li,{children:["config.sensorConfiguration.health.booster: Allow to override default Booster components configuration","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.booster[BOOSTER_COMPONENT_ID].enabled"}),"\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.booster[BOOSTER_COMPONENT_ID].details"}),"\n",(0,s.jsx)(n.li,{children:"config.sensorConfiguration.health.booster[BOOSTER_COMPONENT_ID].showChildren"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"user-components-configuration",children:"User components configuration"}),"\n",(0,s.jsxs)(n.p,{children:["Use ",(0,s.jsx)(n.code,{children:"@HealthSensor"})," parameters to configure user components. Example:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"@HealthSensor({\n id: 'user',\n name: 'my-application',\n enabled: true,\n details: true,\n showChildren: true,\n})\n"})}),"\n",(0,s.jsx)(n.h3,{id:"create-your-own-health-endpoint",children:"Create your own health endpoint"}),"\n",(0,s.jsxs)(n.p,{children:["Create your own health endpoint with a class annotated with ",(0,s.jsx)(n.code,{children:"@HealthSensor"})," decorator. This class\nshould define a ",(0,s.jsx)(n.code,{children:"health"})," method that returns a ",(0,s.jsx)(n.strong,{children:"HealthIndicatorResult"}),". Example:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"import {\n BoosterConfig,\n HealthIndicatorResult,\n HealthIndicatorMetadata,\n HealthStatus,\n} from '@boostercloud/framework-types'\nimport { HealthSensor } from '@boostercloud/framework-core'\n\n@HealthSensor({\n id: 'application',\n name: 'my-application',\n enabled: true,\n details: true,\n showChildren: true,\n})\nexport class ApplicationHealthIndicator {\n public async health(\n config: BoosterConfig,\n healthIndicatorMetadata: HealthIndicatorMetadata\n ): Promise {\n return {\n status: HealthStatus.UP,\n } as HealthIndicatorResult\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"booster-health-endpoints",children:"Booster health endpoints"}),"\n",(0,s.jsx)(n.h4,{id:"booster",children:"booster"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if graphql function is UP and events are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"boosterVersion: Booster version number"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterfunction",children:"booster/function"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if graphql function is UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"graphQL_url: GraphQL function url"}),"\n",(0,s.jsxs)(n.li,{children:["cpus: Information about each logical CPU core.","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["cpu:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"model: Cpu model. Example: AMD EPYC 7763 64-Core Processor"}),"\n",(0,s.jsx)(n.li,{children:"speed: cpu speed in MHz"}),"\n",(0,s.jsxs)(n.li,{children:["times: The number of milliseconds the CPU/core spent in (see iostat)","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"user: CPU utilization that occurred while executing at the user level (application)"}),"\n",(0,s.jsx)(n.li,{children:"nice: CPU utilization that occurred while executing at the user level with nice priority."}),"\n",(0,s.jsx)(n.li,{children:"sys: CPU utilization that occurred while executing at the system level (kernel)."}),"\n",(0,s.jsx)(n.li,{children:"idle: CPU or CPUs were idle and the system did not have an outstanding disk I/O request."}),"\n",(0,s.jsx)(n.li,{children:"irq: CPU load system"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.li,{children:"timesPercentages: For each times value, the percentage over the total times"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["memory:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"totalBytes: the total amount of system memory in bytes as an integer."}),"\n",(0,s.jsx)(n.li,{children:"freeBytes: the amount of free system memory in bytes as an integer."}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterdatabase",children:"booster/database"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if events are UP and Read Models are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"urls: Database urls"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterdatabaseevents",children:"booster/database/events"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if events are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"AZURE PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"url: Events url"}),"\n",(0,s.jsx)(n.li,{children:"count: number of rows"}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"LOCAL PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"file: event database file"}),"\n",(0,s.jsx)(n.li,{children:"count: number of rows"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsx)(n.h4,{id:"boosterdatabasereadmodels",children:"booster/database/readmodels"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"status: UP if and only if Read Models are UP"}),"\n",(0,s.jsxs)(n.li,{children:["details:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"AZURE PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["For each Read Model:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"url: Event url"}),"\n",(0,s.jsx)(n.li,{children:"count: number of rows"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"LOCAL PROVIDER"}),":","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"file: Read Models database file"}),"\n",(0,s.jsx)(n.li,{children:"count: number of total rows"}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Note"}),": details will be included only if ",(0,s.jsx)(n.code,{children:"details"})," is enabled"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"health-status",children:"Health status"}),"\n",(0,s.jsx)(n.p,{children:"Available status are"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"UP: The component or subsystem is working as expected"}),"\n",(0,s.jsx)(n.li,{children:"DOWN: The component is not working"}),"\n",(0,s.jsx)(n.li,{children:"OUT_OF_SERVICE: The component is out of service temporarily"}),"\n",(0,s.jsx)(n.li,{children:"UNKNOWN: The component state is unknown"}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"If a component throw an exception the status will be DOWN"}),"\n",(0,s.jsx)(n.h3,{id:"securing-health-endpoints",children:"Securing health endpoints"}),"\n",(0,s.jsxs)(n.p,{children:["To configure the health endpoints authorization use ",(0,s.jsx)(n.code,{children:"config.sensorConfiguration.health.globalAuthorizer"}),"."]}),"\n",(0,s.jsx)(n.p,{children:"Example:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-typescript",children:"config.sensorConfiguration.health.globalAuthorizer = {\n authorize: 'all',\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the authorization process fails, the health endpoint will return a 401 error code"}),"\n",(0,s.jsx)(n.h3,{id:"example",children:"Example"}),"\n",(0,s.jsx)(n.p,{children:"If all components are enable and showChildren is set to true:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["A Request to ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})," will return:"]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 booster\n\u2502\xa0\xa0\u251c\u2500\u2500 database\n\u2502\xa0\xa0\xa0\xa0\u251c\u2500\u2500 events\n\u2502\xa0\xa0\xa0\xa0\u2514\u2500\u2500 readmodels\n\u2514\xa0\xa0\u2514\u2500\u2500 function\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the database component is disabled, the same url will return:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 booster\n\u2514\xa0\xa0\u2514\u2500\u2500 function\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If the request url is ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database",children:"https://your-application-url/sensor/health/database"}),", the component will not be returned"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"[Empty]\n"})}),"\n",(0,s.jsxs)(n.p,{children:["And the children components will be disabled too using direct url ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database/events",children:"https://your-application-url/sensor/health/database/events"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"[Empty]\n"})}),"\n",(0,s.jsxs)(n.p,{children:["If database is enabled and showChildren is set to false and using ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/",children:"https://your-application-url/sensor/health/"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u251c\u2500\u2500 booster\n\u2502\xa0\xa0\u251c\u2500\u2500 database\n\u2502\xa0\xa0\u2514\u2500\u2500 function\n"})}),"\n",(0,s.jsxs)(n.p,{children:["using ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database",children:"https://your-application-url/sensor/health/database"}),", children will not be visible"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u2514\u2500\u2500 database\n"})}),"\n",(0,s.jsxs)(n.p,{children:["but you can access to them using the component url ",(0,s.jsx)(n.a,{href:"https://your-application-url/sensor/health/database/events",children:"https://your-application-url/sensor/health/database/events"})]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-text",children:"\u2514\u2500\u2500 events\n"})})]})}function d(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>r,a:()=>l});var s=t(7294);const o={},i=s.createContext(o);function l(e){const n=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:l(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/46b77955.5ef13cd0.js b/assets/js/46b77955.5ef13cd0.js deleted file mode 100644 index 8f51bfd1c..000000000 --- a/assets/js/46b77955.5ef13cd0.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[9284],{4302:(t,e,o)=>{o.r(e),o.d(e,{assets:()=>u,contentTitle:()=>i,default:()=>m,frontMatter:()=>a,metadata:()=>c,toc:()=>d});var n=o(5893),s=o(1151),r=o(999);const a={slug:"/"},i="Ask about Booster Framework",c={id:"ai-assistant",title:"Ask about Booster Framework",description:"",source:"@site/docs/00_ai-assistant.md",sourceDirName:".",slug:"/",permalink:"/",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/00_ai-assistant.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:0,frontMatter:{slug:"/"},sidebar:"docs",next:{title:"Introduction",permalink:"/introduction"}},u={},d=[];function l(t){const e={h1:"h1",...(0,s.a)(),...t.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(e.h1,{id:"ask-about-booster-framework",children:"Ask about Booster Framework"}),"\n",(0,n.jsx)(r.ZP,{})]})}function m(t={}){const{wrapper:e}={...(0,s.a)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(l,{...t})}):l(t)}},1151:(t,e,o)=>{o.d(e,{Z:()=>i,a:()=>a});var n=o(7294);const s={},r=n.createContext(s);function a(t){const e=n.useContext(r);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function i(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(s):t.components||s:a(t.components),n.createElement(r.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/46b77955.ed23f00a.js b/assets/js/46b77955.ed23f00a.js new file mode 100644 index 000000000..7f368c43c --- /dev/null +++ b/assets/js/46b77955.ed23f00a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[9284],{4302:(t,e,o)=>{o.r(e),o.d(e,{assets:()=>u,contentTitle:()=>i,default:()=>m,frontMatter:()=>a,metadata:()=>c,toc:()=>d});var n=o(5893),s=o(1151),r=o(999);const a={slug:"/"},i="Ask about Booster Framework",c={id:"ai-assistant",title:"Ask about Booster Framework",description:"",source:"@site/docs/00_ai-assistant.md",sourceDirName:".",slug:"/",permalink:"/",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/00_ai-assistant.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:0,frontMatter:{slug:"/"},sidebar:"docs",next:{title:"Introduction",permalink:"/introduction"}},u={},d=[];function l(t){const e={h1:"h1",...(0,s.a)(),...t.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(e.h1,{id:"ask-about-booster-framework",children:"Ask about Booster Framework"}),"\n",(0,n.jsx)(r.ZP,{})]})}function m(t={}){const{wrapper:e}={...(0,s.a)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(l,{...t})}):l(t)}},1151:(t,e,o)=>{o.d(e,{Z:()=>i,a:()=>a});var n=o(7294);const s={},r=n.createContext(s);function a(t){const e=n.useContext(r);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function i(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(s):t.components||s:a(t.components),n.createElement(r.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/4da0bd64.080193ec.js b/assets/js/4da0bd64.080193ec.js new file mode 100644 index 000000000..fafb9c8a3 --- /dev/null +++ b/assets/js/4da0bd64.080193ec.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6038],{5277:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>c,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>d});var o=t(5893),r=t(1151);const a={},c="Booster instrumentation",s={id:"going-deeper/instrumentation",title:"Booster instrumentation",description:"Trace Decorator",source:"@site/docs/10_going-deeper/instrumentation.md",sourceDirName:"10_going-deeper",slug:"/going-deeper/instrumentation",permalink:"/going-deeper/instrumentation",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/instrumentation.md",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Framework packages",permalink:"/going-deeper/framework-packages"},next:{title:"Scaling Booster Azure Functions",permalink:"/going-deeper/azure-scale"}},i={},d=[{value:"Trace Decorator",id:"trace-decorator",level:2},{value:"Usage",id:"usage",level:3},{value:"TraceActionTypes",id:"traceactiontypes",level:3},{value:"TraceInfo",id:"traceinfo",level:3},{value:"Adding the Trace Decorator to Your own async methods",id:"adding-the-trace-decorator-to-your-own-async-methods",level:3}];function l(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,r.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"booster-instrumentation",children:"Booster instrumentation"}),"\n",(0,o.jsx)(n.h2,{id:"trace-decorator",children:"Trace Decorator"}),"\n",(0,o.jsxs)(n.p,{children:["The Trace Decorator is a ",(0,o.jsx)(n.strong,{children:"Booster"})," functionality that facilitates the reception of notifications whenever significant events occur in Booster's core, such as event dispatching or migration execution."]}),"\n",(0,o.jsx)(n.h3,{id:"usage",children:"Usage"}),"\n",(0,o.jsx)(n.p,{children:"To configure a custom tracer, you need to define an object with two methods: onStart and onEnd. The onStart method is called before the traced method is invoked, and the onEnd method is called after the method completes. Both methods receive a TraceInfo object, which contains information about the traced method and its arguments."}),"\n",(0,o.jsx)(n.p,{children:"Here's an example of a custom tracer that logs trace events to the console:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import {\n TraceParameters,\n BoosterConfig,\n TraceActionTypes,\n} from '@boostercloud/framework-types'\n\nclass MyTracer {\n static async onStart(config: BoosterConfig, actionType: string, traceParameters: TraceParameters): Promise {\n console.log(`Start ${actionType}: ${traceParameters.className}.${traceParameters.methodName}`)\n }\n\n static async onEnd(config: BoosterConfig, actionType: string, traceParameters: TraceParameters): Promise {\n console.log(`End ${actionType}: ${traceParameters.className}.${traceParameters.methodName}`)\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"You can then configure the tracer in your Booster application's configuration:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { BoosterConfig } from '@boostercloud/framework-types'\nimport { MyTracer } from './my-tracer'\n\nconst config: BoosterConfig = {\n// ...other configuration options...\n trace: {\n enableTraceNotification: true,\n onStart: MyTracer.onStart,\n onEnd: MyTracer.onStart,\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"In the configuration above, we've enabled trace notifications and specified our onStart and onEnd as the methods to use. Verbose disable will reduce the amount of information generated excluding the internal parameter in the trace parameters."}),"\n",(0,o.jsxs)(n.p,{children:["Setting ",(0,o.jsx)(n.code,{children:"enableTraceNotification: true"})," would enable the trace for all actions. You can either disable them by setting it to ",(0,o.jsx)(n.code,{children:"false"})," or selectively enable only specific actions using an array of TraceActionTypes."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { BoosterConfig, TraceActionTypes } from '@boostercloud/framework-types'\nimport { MyTracer } from './my-tracer'\n\nconst config: BoosterConfig = {\n// ...other configuration options...\n trace: {\n enableTraceNotification: [TraceActionTypes.DISPATCH_EVENT, TraceActionTypes.MIGRATION_RUN, 'OTHER'],\n includeInternal: false,\n onStart: MyTracer.onStart,\n onEnd: MyTracer.onStart,\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"In this example, only DISPATCH_EVENT, MIGRATION_RUN and 'OTHER' actions will trigger trace notifications."}),"\n",(0,o.jsx)(n.h3,{id:"traceactiontypes",children:"TraceActionTypes"}),"\n",(0,o.jsx)(n.p,{children:"The TraceActionTypes enum defines all the traceable actions in Booster's core:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"export enum TraceActionTypes {\n CUSTOM,\n EVENT_HANDLERS_PROCESS,\n HANDLE_EVENT,\n DISPATCH_ENTITY_TO_EVENT_HANDLERS,\n DISPATCH_EVENTS,\n FETCH_ENTITY_SNAPSHOT,\n STORE_SNAPSHOT,\n LOAD_LATEST_SNAPSHOT,\n LOAD_EVENT_STREAM_SINCE,\n ENTITY_REDUCER,\n READ_MODEL_FIND_BY_ID,\n GRAPHQL_READ_MODEL_SEARCH,\n READ_MODEL_SEARCH,\n COMMAND_HANDLER,\n MIGRATION_RUN,\n GRAPHQL_DISPATCH,\n GRAPHQL_RUN_OPERATION,\n SCHEDULED_COMMAND_HANDLER,\n DISPATCH_SUBSCRIBER_NOTIFIER,\n READ_MODEL_SCHEMA_MIGRATOR_RUN,\n SCHEMA_MIGRATOR_MIGRATE,\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"traceinfo",children:"TraceInfo"}),"\n",(0,o.jsx)(n.p,{children:"The TraceInfo interface defines the data that is passed to the tracer's onBefore and onAfter methods:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"export interface TraceInfo {\n className: string\n methodName: string\n args: Array\n traceId: UUID\n elapsedInvocationMillis?: number\n internal: {\n target: unknown\n descriptor: PropertyDescriptor\n }\n description?: string\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.code,{children:"className"})," and ",(0,o.jsx)(n.code,{children:"methodName"})," identify the function that is being traced."]}),"\n",(0,o.jsx)(n.h3,{id:"adding-the-trace-decorator-to-your-own-async-methods",children:"Adding the Trace Decorator to Your own async methods"}),"\n",(0,o.jsx)(n.p,{children:"In addition to using the Trace Decorator to receive notifications when events occur in Booster's core, you can also use it to trace your own methods. To add the Trace Decorator to your own methods, simply add @Trace() before your method declaration."}),"\n",(0,o.jsx)(n.p,{children:"Here's an example of how to use the Trace Decorator on a custom method:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Trace } from '@boostercloud/framework-core'\nimport { BoosterConfig, Logger } from '@boostercloud/framework-types'\n\nexport class MyCustomClass {\n @Trace('OTHER')\n public async myCustomMethod(config: BoosterConfig, logger: Logger): Promise {\n logger.debug('This is my custom method')\n // Do some custom logic here...\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"In the example above, we added the @Trace('OTHER') decorator to the myCustomMethod method. This will cause the method to emit trace events when it's invoked, allowing you to trace the flow of your application and detect performance bottlenecks or errors."}),"\n",(0,o.jsx)(n.p,{children:"Note that when you add the Trace Decorator to your own methods, you'll need to configure your Booster instance to use a tracer that implements the necessary methods to handle these events."})]})}function h(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>s,a:()=>c});var o=t(7294);const r={},a=o.createContext(r);function c(e){const n=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),o.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/4da0bd64.8de1ac1a.js b/assets/js/4da0bd64.8de1ac1a.js deleted file mode 100644 index ffc40654b..000000000 --- a/assets/js/4da0bd64.8de1ac1a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6038],{5277:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>i,contentTitle:()=>c,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>d});var o=t(5893),r=t(1151);const a={},c="Booster instrumentation",s={id:"going-deeper/instrumentation",title:"Booster instrumentation",description:"Trace Decorator",source:"@site/docs/10_going-deeper/instrumentation.md",sourceDirName:"10_going-deeper",slug:"/going-deeper/instrumentation",permalink:"/going-deeper/instrumentation",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/10_going-deeper/instrumentation.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",frontMatter:{},sidebar:"docs",previous:{title:"Framework packages",permalink:"/going-deeper/framework-packages"},next:{title:"Scaling Booster Azure Functions",permalink:"/going-deeper/azure-scale"}},i={},d=[{value:"Trace Decorator",id:"trace-decorator",level:2},{value:"Usage",id:"usage",level:3},{value:"TraceActionTypes",id:"traceactiontypes",level:3},{value:"TraceInfo",id:"traceinfo",level:3},{value:"Adding the Trace Decorator to Your own async methods",id:"adding-the-trace-decorator-to-your-own-async-methods",level:3}];function l(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",strong:"strong",...(0,r.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"booster-instrumentation",children:"Booster instrumentation"}),"\n",(0,o.jsx)(n.h2,{id:"trace-decorator",children:"Trace Decorator"}),"\n",(0,o.jsxs)(n.p,{children:["The Trace Decorator is a ",(0,o.jsx)(n.strong,{children:"Booster"})," functionality that facilitates the reception of notifications whenever significant events occur in Booster's core, such as event dispatching or migration execution."]}),"\n",(0,o.jsx)(n.h3,{id:"usage",children:"Usage"}),"\n",(0,o.jsx)(n.p,{children:"To configure a custom tracer, you need to define an object with two methods: onStart and onEnd. The onStart method is called before the traced method is invoked, and the onEnd method is called after the method completes. Both methods receive a TraceInfo object, which contains information about the traced method and its arguments."}),"\n",(0,o.jsx)(n.p,{children:"Here's an example of a custom tracer that logs trace events to the console:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import {\n TraceParameters,\n BoosterConfig,\n TraceActionTypes,\n} from '@boostercloud/framework-types'\n\nclass MyTracer {\n static async onStart(config: BoosterConfig, actionType: string, traceParameters: TraceParameters): Promise {\n console.log(`Start ${actionType}: ${traceParameters.className}.${traceParameters.methodName}`)\n }\n\n static async onEnd(config: BoosterConfig, actionType: string, traceParameters: TraceParameters): Promise {\n console.log(`End ${actionType}: ${traceParameters.className}.${traceParameters.methodName}`)\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"You can then configure the tracer in your Booster application's configuration:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { BoosterConfig } from '@boostercloud/framework-types'\nimport { MyTracer } from './my-tracer'\n\nconst config: BoosterConfig = {\n// ...other configuration options...\n trace: {\n enableTraceNotification: true,\n onStart: MyTracer.onStart,\n onEnd: MyTracer.onStart,\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"In the configuration above, we've enabled trace notifications and specified our onStart and onEnd as the methods to use. Verbose disable will reduce the amount of information generated excluding the internal parameter in the trace parameters."}),"\n",(0,o.jsxs)(n.p,{children:["Setting ",(0,o.jsx)(n.code,{children:"enableTraceNotification: true"})," would enable the trace for all actions. You can either disable them by setting it to ",(0,o.jsx)(n.code,{children:"false"})," or selectively enable only specific actions using an array of TraceActionTypes."]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { BoosterConfig, TraceActionTypes } from '@boostercloud/framework-types'\nimport { MyTracer } from './my-tracer'\n\nconst config: BoosterConfig = {\n// ...other configuration options...\n trace: {\n enableTraceNotification: [TraceActionTypes.DISPATCH_EVENT, TraceActionTypes.MIGRATION_RUN, 'OTHER'],\n includeInternal: false,\n onStart: MyTracer.onStart,\n onEnd: MyTracer.onStart,\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"In this example, only DISPATCH_EVENT, MIGRATION_RUN and 'OTHER' actions will trigger trace notifications."}),"\n",(0,o.jsx)(n.h3,{id:"traceactiontypes",children:"TraceActionTypes"}),"\n",(0,o.jsx)(n.p,{children:"The TraceActionTypes enum defines all the traceable actions in Booster's core:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"export enum TraceActionTypes {\n CUSTOM,\n EVENT_HANDLERS_PROCESS,\n HANDLE_EVENT,\n DISPATCH_ENTITY_TO_EVENT_HANDLERS,\n DISPATCH_EVENTS,\n FETCH_ENTITY_SNAPSHOT,\n STORE_SNAPSHOT,\n LOAD_LATEST_SNAPSHOT,\n LOAD_EVENT_STREAM_SINCE,\n ENTITY_REDUCER,\n READ_MODEL_FIND_BY_ID,\n GRAPHQL_READ_MODEL_SEARCH,\n READ_MODEL_SEARCH,\n COMMAND_HANDLER,\n MIGRATION_RUN,\n GRAPHQL_DISPATCH,\n GRAPHQL_RUN_OPERATION,\n SCHEDULED_COMMAND_HANDLER,\n DISPATCH_SUBSCRIBER_NOTIFIER,\n READ_MODEL_SCHEMA_MIGRATOR_RUN,\n SCHEMA_MIGRATOR_MIGRATE,\n}\n"})}),"\n",(0,o.jsx)(n.h3,{id:"traceinfo",children:"TraceInfo"}),"\n",(0,o.jsx)(n.p,{children:"The TraceInfo interface defines the data that is passed to the tracer's onBefore and onAfter methods:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"export interface TraceInfo {\n className: string\n methodName: string\n args: Array\n traceId: UUID\n elapsedInvocationMillis?: number\n internal: {\n target: unknown\n descriptor: PropertyDescriptor\n }\n description?: string\n}\n"})}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.code,{children:"className"})," and ",(0,o.jsx)(n.code,{children:"methodName"})," identify the function that is being traced."]}),"\n",(0,o.jsx)(n.h3,{id:"adding-the-trace-decorator-to-your-own-async-methods",children:"Adding the Trace Decorator to Your own async methods"}),"\n",(0,o.jsx)(n.p,{children:"In addition to using the Trace Decorator to receive notifications when events occur in Booster's core, you can also use it to trace your own methods. To add the Trace Decorator to your own methods, simply add @Trace() before your method declaration."}),"\n",(0,o.jsx)(n.p,{children:"Here's an example of how to use the Trace Decorator on a custom method:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-typescript",children:"import { Trace } from '@boostercloud/framework-core'\nimport { BoosterConfig, Logger } from '@boostercloud/framework-types'\n\nexport class MyCustomClass {\n @Trace('OTHER')\n public async myCustomMethod(config: BoosterConfig, logger: Logger): Promise {\n logger.debug('This is my custom method')\n // Do some custom logic here...\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"In the example above, we added the @Trace('OTHER') decorator to the myCustomMethod method. This will cause the method to emit trace events when it's invoked, allowing you to trace the flow of your application and detect performance bottlenecks or errors."}),"\n",(0,o.jsx)(n.p,{children:"Note that when you add the Trace Decorator to your own methods, you'll need to configure your Booster instance to use a tracer that implements the necessary methods to handle these events."})]})}function h(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>s,a:()=>c});var o=t(7294);const r={},a=o.createContext(r);function c(e){const n=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),o.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/55aa456f.27107eab.js b/assets/js/55aa456f.27107eab.js deleted file mode 100644 index 7aecd51f4..000000000 --- a/assets/js/55aa456f.27107eab.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[695],{4663:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>o,default:()=>v,frontMatter:()=>s,metadata:()=>d,toc:()=>c});var r=t(5893),a=t(1151),i=t(5163);const s={description:"Learn how to react to events and trigger side effects in Booster by defining event handlers."},o="Event handler",d={id:"architecture/event-handler",title:"Event handler",description:"Learn how to react to events and trigger side effects in Booster by defining event handlers.",source:"@site/docs/03_architecture/04_event-handler.mdx",sourceDirName:"03_architecture",slug:"/architecture/event-handler",permalink:"/architecture/event-handler",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/04_event-handler.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:4,frontMatter:{description:"Learn how to react to events and trigger side effects in Booster by defining event handlers."},sidebar:"docs",previous:{title:"Event",permalink:"/architecture/event"},next:{title:"Entity",permalink:"/architecture/entity"}},l={},c=[{value:"Creating an event handler",id:"creating-an-event-handler",level:2},{value:"Declaring an event handler",id:"declaring-an-event-handler",level:2},{value:"Creating an event handler",id:"creating-an-event-handler-1",level:2},{value:"Registering events from an event handler",id:"registering-events-from-an-event-handler",level:2},{value:"Reading entities from event handlers",id:"reading-entities-from-event-handlers",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"event-handler",children:"Event handler"}),"\n",(0,r.jsx)(n.p,{children:"An event handler is a class that reacts to events. They are commonly used to trigger side effects in case of a new event. For instance, if a new event is registered in the system, an event handler could send an email to the user."}),"\n",(0,r.jsx)(n.h2,{id:"creating-an-event-handler",children:"Creating an event handler"}),"\n",(0,r.jsx)(n.p,{children:"The Booster CLI will help you to create new event handlers. You just need to run the following command and the CLI will generate all the boilerplate for you:"}),"\n",(0,r.jsx)(i.Z,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,r.jsxs)(n.p,{children:["This will generate a new file called ",(0,r.jsx)(n.code,{children:"handle-availability.ts"})," in the ",(0,r.jsx)(n.code,{children:"src/event-handlers"})," directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI."]}),"\n",(0,r.jsx)(n.h2,{id:"declaring-an-event-handler",children:"Declaring an event handler"}),"\n",(0,r.jsxs)(n.p,{children:["In Booster, event handlers are classes decorated with the ",(0,r.jsx)(n.code,{children:"@EventHandler"})," decorator. The parameter of the decorator is the event that the handler will react to. The logic to be triggered after an event is registered is defined in the ",(0,r.jsx)(n.code,{children:"handle"})," method of the class. This ",(0,r.jsx)(n.code,{children:"handle"})," function will receive the event that triggered the handler."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/event-handlers/handle-availability.ts"',children:"// highlight-next-line\n@EventHandler(StockMoved)\nexport class HandleAvailability {\n // highlight-start\n public static async handle(event: StockMoved): Promise {\n // Do something here\n }\n // highlight-end\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"creating-an-event-handler-1",children:"Creating an event handler"}),"\n",(0,r.jsxs)(n.p,{children:["Event handlers can be easily created using the Booster CLI command ",(0,r.jsx)(n.code,{children:"boost new:event-handler"}),". There are two mandatory arguments: the event handler name, and the name of the event it will react to. For instance:"]}),"\n",(0,r.jsx)(i.Z,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,r.jsxs)(n.p,{children:["Once the creation is completed, there will be a new file in the event handlers directory ",(0,r.jsx)(n.code,{children:"/src/event-handlers/handle-availability.ts"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"\n\u251c\u2500\u2500 src\n\u2502 \u251c\u2500\u2500 commands\n\u2502 \u251c\u2500\u2500 common\n\u2502 \u251c\u2500\u2500 config\n\u2502 \u251c\u2500\u2500 entities\n\u2502 \u251c\u2500\u2500 events\n\u2502 \u251c\u2500\u2500 event-handlers <------ put them here\n\u2502 \u2514\u2500\u2500 read-models\n"})}),"\n",(0,r.jsx)(n.h2,{id:"registering-events-from-an-event-handler",children:"Registering events from an event handler"}),"\n",(0,r.jsx)(n.p,{children:"Event handlers can also register new events. This is useful when you want to trigger a new event after a certain condition is met. For example, if you want to send an email to the user when a product is out of stock."}),"\n",(0,r.jsxs)(n.p,{children:["In order to register new events, Booster injects the ",(0,r.jsx)(n.code,{children:"register"})," instance in the ",(0,r.jsx)(n.code,{children:"handle"})," method as a second parameter. This ",(0,r.jsx)(n.code,{children:"register"})," instance has a ",(0,r.jsx)(n.code,{children:"events(...)"})," method that allows you to store any side effect events, you can specify as many as you need separated by commas as arguments of the function."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/event-handlers/handle-availability.ts"',children:"@EventHandler(StockMoved)\nexport class HandleAvailability {\n public static async handle(event: StockMoved, register: Register): Promise {\n if (event.quantity < 0) {\n // highlight-next-line\n register.events([new ProductOutOfStock(event.productID)])\n }\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"reading-entities-from-event-handlers",children:"Reading entities from event handlers"}),"\n",(0,r.jsx)(n.p,{children:"There are cases where you need to read an entity to make a decision based on its current state. Different side effects can be triggered depending on the current state of the entity. Given the previous example, if a user does not want to receive emails when a product is out of stock, we should be able check the user preferences before sending the email."}),"\n",(0,r.jsxs)(n.p,{children:["For that reason, Booster provides the ",(0,r.jsx)(n.code,{children:"Booster.entity"})," function. This function allows you to retrieve the current state of an entity. Let's say that we want to check the status of a product before we trigger its availability update. In that case we would call the ",(0,r.jsx)(n.code,{children:"Booster.entity"})," function, which will return information about the entity."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/event-handlers/handle-availability.ts"',children:"@EventHandler(StockMoved)\nexport class HandleAvailability {\n public static async handle(event: StockMoved, register: Register): Promise {\n // highlight-next-line\n const product = await Booster.entity(Product, event.productID)\n if (product.stock < 0) {\n register.events([new ProductOutOfStock(event.productID)])\n }\n }\n}\n"})})]})}function v(e={}){const{wrapper:n}={...(0,a.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},5163:(e,n,t)=>{t.d(n,{Z:()=>i});t(7294);const r={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var a=t(5893);function i(e){let{children:n}=e;return(0,a.jsxs)("div",{className:r.terminalWindow,children:[(0,a.jsx)("div",{className:r.terminalWindowHeader,children:(0,a.jsxs)("div",{className:r.buttons,children:[(0,a.jsx)("span",{className:r.dot,style:{background:"#f25f58"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#fbbe3c"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#58cb42"}})]})}),(0,a.jsx)("div",{className:r.terminalWindowBody,children:n})]})}},1151:(e,n,t)=>{t.d(n,{Z:()=>o,a:()=>s});var r=t(7294);const a={},i=r.createContext(a);function s(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/55aa456f.8e49a0fa.js b/assets/js/55aa456f.8e49a0fa.js new file mode 100644 index 000000000..8d3c7edeb --- /dev/null +++ b/assets/js/55aa456f.8e49a0fa.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[695],{4663:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>v,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var r=t(5893),a=t(1151),i=t(5163);const s={description:"Learn how to react to events and trigger side effects in Booster by defining event handlers."},d="Event handler",o={id:"architecture/event-handler",title:"Event handler",description:"Learn how to react to events and trigger side effects in Booster by defining event handlers.",source:"@site/docs/03_architecture/04_event-handler.mdx",sourceDirName:"03_architecture",slug:"/architecture/event-handler",permalink:"/architecture/event-handler",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/04_event-handler.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:4,frontMatter:{description:"Learn how to react to events and trigger side effects in Booster by defining event handlers."},sidebar:"docs",previous:{title:"Event",permalink:"/architecture/event"},next:{title:"Entity",permalink:"/architecture/entity"}},l={},c=[{value:"Creating an event handler",id:"creating-an-event-handler",level:2},{value:"Declaring an event handler",id:"declaring-an-event-handler",level:2},{value:"Creating an event handler",id:"creating-an-event-handler-1",level:2},{value:"Registering events from an event handler",id:"registering-events-from-an-event-handler",level:2},{value:"Reading entities from event handlers",id:"reading-entities-from-event-handlers",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"event-handler",children:"Event handler"}),"\n",(0,r.jsx)(n.p,{children:"An event handler is a class that reacts to events. They are commonly used to trigger side effects in case of a new event. For instance, if a new event is registered in the system, an event handler could send an email to the user."}),"\n",(0,r.jsx)(n.h2,{id:"creating-an-event-handler",children:"Creating an event handler"}),"\n",(0,r.jsx)(n.p,{children:"The Booster CLI will help you to create new event handlers. You just need to run the following command and the CLI will generate all the boilerplate for you:"}),"\n",(0,r.jsx)(i.Z,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,r.jsxs)(n.p,{children:["This will generate a new file called ",(0,r.jsx)(n.code,{children:"handle-availability.ts"})," in the ",(0,r.jsx)(n.code,{children:"src/event-handlers"})," directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI."]}),"\n",(0,r.jsx)(n.h2,{id:"declaring-an-event-handler",children:"Declaring an event handler"}),"\n",(0,r.jsxs)(n.p,{children:["In Booster, event handlers are classes decorated with the ",(0,r.jsx)(n.code,{children:"@EventHandler"})," decorator. The parameter of the decorator is the event that the handler will react to. The logic to be triggered after an event is registered is defined in the ",(0,r.jsx)(n.code,{children:"handle"})," method of the class. This ",(0,r.jsx)(n.code,{children:"handle"})," function will receive the event that triggered the handler."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/event-handlers/handle-availability.ts"',children:"// highlight-next-line\n@EventHandler(StockMoved)\nexport class HandleAvailability {\n // highlight-start\n public static async handle(event: StockMoved): Promise {\n // Do something here\n }\n // highlight-end\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"creating-an-event-handler-1",children:"Creating an event handler"}),"\n",(0,r.jsxs)(n.p,{children:["Event handlers can be easily created using the Booster CLI command ",(0,r.jsx)(n.code,{children:"boost new:event-handler"}),". There are two mandatory arguments: the event handler name, and the name of the event it will react to. For instance:"]}),"\n",(0,r.jsx)(i.Z,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,r.jsxs)(n.p,{children:["Once the creation is completed, there will be a new file in the event handlers directory ",(0,r.jsx)(n.code,{children:"/src/event-handlers/handle-availability.ts"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"\n\u251c\u2500\u2500 src\n\u2502 \u251c\u2500\u2500 commands\n\u2502 \u251c\u2500\u2500 common\n\u2502 \u251c\u2500\u2500 config\n\u2502 \u251c\u2500\u2500 entities\n\u2502 \u251c\u2500\u2500 events\n\u2502 \u251c\u2500\u2500 event-handlers <------ put them here\n\u2502 \u2514\u2500\u2500 read-models\n"})}),"\n",(0,r.jsx)(n.h2,{id:"registering-events-from-an-event-handler",children:"Registering events from an event handler"}),"\n",(0,r.jsx)(n.p,{children:"Event handlers can also register new events. This is useful when you want to trigger a new event after a certain condition is met. For example, if you want to send an email to the user when a product is out of stock."}),"\n",(0,r.jsxs)(n.p,{children:["In order to register new events, Booster injects the ",(0,r.jsx)(n.code,{children:"register"})," instance in the ",(0,r.jsx)(n.code,{children:"handle"})," method as a second parameter. This ",(0,r.jsx)(n.code,{children:"register"})," instance has a ",(0,r.jsx)(n.code,{children:"events(...)"})," method that allows you to store any side effect events, you can specify as many as you need separated by commas as arguments of the function."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/event-handlers/handle-availability.ts"',children:"@EventHandler(StockMoved)\nexport class HandleAvailability {\n public static async handle(event: StockMoved, register: Register): Promise {\n if (event.quantity < 0) {\n // highlight-next-line\n register.events([new ProductOutOfStock(event.productID)])\n }\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"reading-entities-from-event-handlers",children:"Reading entities from event handlers"}),"\n",(0,r.jsx)(n.p,{children:"There are cases where you need to read an entity to make a decision based on its current state. Different side effects can be triggered depending on the current state of the entity. Given the previous example, if a user does not want to receive emails when a product is out of stock, we should be able check the user preferences before sending the email."}),"\n",(0,r.jsxs)(n.p,{children:["For that reason, Booster provides the ",(0,r.jsx)(n.code,{children:"Booster.entity"})," function. This function allows you to retrieve the current state of an entity. Let's say that we want to check the status of a product before we trigger its availability update. In that case we would call the ",(0,r.jsx)(n.code,{children:"Booster.entity"})," function, which will return information about the entity."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/event-handlers/handle-availability.ts"',children:"@EventHandler(StockMoved)\nexport class HandleAvailability {\n public static async handle(event: StockMoved, register: Register): Promise {\n // highlight-next-line\n const product = await Booster.entity(Product, event.productID)\n if (product.stock < 0) {\n register.events([new ProductOutOfStock(event.productID)])\n }\n }\n}\n"})})]})}function v(e={}){const{wrapper:n}={...(0,a.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},5163:(e,n,t)=>{t.d(n,{Z:()=>i});t(7294);const r={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var a=t(5893);function i(e){let{children:n}=e;return(0,a.jsxs)("div",{className:r.terminalWindow,children:[(0,a.jsx)("div",{className:r.terminalWindowHeader,children:(0,a.jsxs)("div",{className:r.buttons,children:[(0,a.jsx)("span",{className:r.dot,style:{background:"#f25f58"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#fbbe3c"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#58cb42"}})]})}),(0,a.jsx)("div",{className:r.terminalWindowBody,children:n})]})}},1151:(e,n,t)=>{t.d(n,{Z:()=>d,a:()=>s});var r=t(7294);const a={},i=r.createContext(a);function s(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/5b078add.5ff158e9.js b/assets/js/5b078add.5ff158e9.js deleted file mode 100644 index 6fade71da..000000000 --- a/assets/js/5b078add.5ff158e9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1422],{4074:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>s,default:()=>h,frontMatter:()=>i,metadata:()=>c,toc:()=>l});var r=t(5893),a=t(1151),o=t(5163);const i={},s="Command",c={id:"architecture/command",title:"Command",description:"Commands are any action a user performs on your application. For example, RemoveItemFromCart, RatePhoto or AddCommentToPost. They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a request on a REST API. Command issuers can also send data on a command as parameters.",source:"@site/docs/03_architecture/02_command.mdx",sourceDirName:"03_architecture",slug:"/architecture/command",permalink:"/architecture/command",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/02_command.mdx",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:2,frontMatter:{},sidebar:"docs",previous:{title:"Booster architecture",permalink:"/architecture/event-driven"},next:{title:"Event",permalink:"/architecture/event"}},d={},l=[{value:"Creating a command",id:"creating-a-command",level:2},{value:"Declaring a command",id:"declaring-a-command",level:2},{value:"The command handler function",id:"the-command-handler-function",level:2},{value:"Registering events",id:"registering-events",level:3},{value:"Returning a value",id:"returning-a-value",level:3},{value:"Validating data",id:"validating-data",level:3},{value:"Throw an error",id:"throw-an-error",level:4},{value:"Register error events",id:"register-error-events",level:4},{value:"Reading entities",id:"reading-entities",level:3},{value:"Authorizing a command",id:"authorizing-a-command",level:2},{value:"Submitting a command",id:"submitting-a-command",level:2},{value:"Commands naming convention",id:"commands-naming-convention",level:2}];function m(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"command",children:"Command"}),"\n",(0,r.jsxs)(n.p,{children:["Commands are any action a user performs on your application. For example, ",(0,r.jsx)(n.code,{children:"RemoveItemFromCart"}),", ",(0,r.jsx)(n.code,{children:"RatePhoto"})," or ",(0,r.jsx)(n.code,{children:"AddCommentToPost"}),". They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a ",(0,r.jsx)(n.strong,{children:"request on a REST API"}),". Command issuers can also send data on a command as parameters."]}),"\n",(0,r.jsx)(n.h2,{id:"creating-a-command",children:"Creating a command"}),"\n",(0,r.jsx)(n.p,{children:"The Booster CLI will help you to create new commands. You just need to run the following command and the CLI will generate all the boilerplate for you:"}),"\n",(0,r.jsx)(o.Z,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:"boost new:command CreateProduct --fields sku:SKU displayName:string description:string price:Money\n"})})}),"\n",(0,r.jsxs)(n.p,{children:["This will generate a new file called ",(0,r.jsx)(n.code,{children:"create-product"})," in the ",(0,r.jsx)(n.code,{children:"src/commands"})," directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI."]}),"\n",(0,r.jsx)(n.h2,{id:"declaring-a-command",children:"Declaring a command"}),"\n",(0,r.jsxs)(n.p,{children:["In Booster you define them as TypeScript classes decorated with the ",(0,r.jsx)(n.code,{children:"@Command"})," decorator. The ",(0,r.jsx)(n.code,{children:"Command"})," parameters will be declared as properties of the class."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/command-name.ts"',children:"@Command()\nexport class CommandName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType) {}\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["These commands are handled by ",(0,r.jsx)(n.code,{children:"Command Handlers"}),", the same way a ",(0,r.jsx)(n.strong,{children:"REST Controller"})," do with a request. To create a ",(0,r.jsx)(n.code,{children:"Command handler"})," of a specific Command, you must declare a ",(0,r.jsx)(n.code,{children:"handle"})," class function inside the corresponding command you want to handle. For example:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/command-name.ts"',children:"@Command()\nexport class CommandName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType) {}\n\n // highlight-start\n public static async handle(command: CommandName, register: Register): Promise {\n // Validate inputs\n // Run domain logic\n // register.events([event1,...])\n }\n // highlight-end\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Booster will then generate the GraphQL mutation for the corresponding command, and the infrastructure to handle them. You only have to define the class and the handler function. Commands are part of the public API, so you can define authorization policies for them, you can read more about this on ",(0,r.jsx)(n.a,{href:"/security/authorization",children:"the authorization section"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"We recommend using command handlers to validate input data before registering events into the event store because they are immutable once there."})}),"\n",(0,r.jsx)(n.h2,{id:"the-command-handler-function",children:"The command handler function"}),"\n",(0,r.jsxs)(n.p,{children:["Each command class must have a method called ",(0,r.jsx)(n.code,{children:"handle"}),". This function is the command handler, and it will be called by the framework every time one instance of this command is submitted. Inside the handler you can run validations, return errors, query entities to make decisions, and register relevant domain events."]}),"\n",(0,r.jsx)(n.h3,{id:"registering-events",children:"Registering events"}),"\n",(0,r.jsxs)(n.p,{children:["Within the command handler execution, it is possible to register domain events. The command handler function receives the ",(0,r.jsx)(n.code,{children:"register"})," argument, so within the handler, it is possible to call ",(0,r.jsx)(n.code,{children:"register.events(...)"})," with a list of events."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command()\nexport class CreateProduct {\n public constructor(readonly sku: string, readonly price: number) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n // highlight-next-line\n register.event(new ProductCreated(/*...*/))\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["For more details about events and the register parameter, see the ",(0,r.jsx)(n.a,{href:"/architecture/event",children:(0,r.jsx)(n.code,{children:"Events"})})," section."]}),"\n",(0,r.jsx)(n.h3,{id:"returning-a-value",children:"Returning a value"}),"\n",(0,r.jsxs)(n.p,{children:["The command handler function can return a value. This value will be the response of the GraphQL mutation. By default, the command handler function expects you to return a ",(0,r.jsx)(n.code,{children:"void"})," as a return type. Since GrahpQL does not have a ",(0,r.jsx)(n.code,{children:"void"})," type, the command handler function returns ",(0,r.jsx)(n.code,{children:"true"})," when called through the GraphQL. This is because the GraphQL specification requires a response, and ",(0,r.jsx)(n.code,{children:"true"})," is the most appropriate value to represent a successful execution with no return value."]}),"\n",(0,r.jsxs)(n.p,{children:["If you want to return a value, you can change the return type of the handler function. For example, if you want to return a ",(0,r.jsx)(n.code,{children:"string"}),":"]}),"\n",(0,r.jsx)(n.p,{children:"For example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command()\nexport class CreateProduct {\n public constructor(readonly sku: string, readonly price: number) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n register.event(new ProductCreated(/*...*/))\n // highlight-next-line\n return 'Product created!'\n }\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"validating-data",children:"Validating data"}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["Booster uses the typed nature of GraphQL to ensure that types are correct before reaching the handler, so ",(0,r.jsx)(n.strong,{children:"you don't have to validate types"}),"."]})}),"\n",(0,r.jsx)(n.h4,{id:"throw-an-error",children:"Throw an error"}),"\n",(0,r.jsx)(n.p,{children:"A command will fail if there is an uncaught error during its handling. When a command fails, Booster will return a detailed error response with the message of the thrown error. This is useful for debugging, but it is also a security feature. Booster will never return an error stack trace to the client, so you don't have to worry about exposing internal implementation details."}),"\n",(0,r.jsx)(n.p,{children:"One case where you might want to throw an error is when the command is invalid because it breaks a business rule. For example, if the command contains a negative price. In that case, you can throw an error in the handler. Booster will use the error's message as the response to make it descriptive. For example, given this command:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command()\nexport class CreateProduct {\n public constructor(readonly sku: string, readonly price: number) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n const priceLimit = 10\n if (command.price >= priceLimit) {\n // highlight-next-line\n throw new Error(`price must be below ${priceLimit}, and it was ${command.price}`)\n }\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"You'll get something like this response:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "errors": [\n {\n "message": "price must be below 10, and it was 19.99",\n "path": ["CreateProduct"]\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"register-error-events",children:"Register error events"}),"\n",(0,r.jsx)(n.p,{children:"There could be situations in which you want to register an event representing an error. For example, when moving items with insufficient stock from one location to another:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/move-stock.ts"',children:"@Command()\nexport class MoveStock {\n public constructor(\n readonly productID: string,\n readonly origin: string,\n readonly destination: string,\n readonly quantity: number\n ) {}\n\n public static async handle(command: MoveStock, register: Register): Promise {\n if (!command.enoughStock(command.productID, command.origin, command.quantity)) {\n // highlight-next-line\n register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))\n } else {\n register.events(new StockMoved(/*...*/))\n }\n }\n\n private enoughStock(productID: string, origin: string, quantity: number): boolean {\n /* ... */\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"In this case, the command operation can still be completed. An event handler will take care of that `ErrorEvent and proceed accordingly."}),"\n",(0,r.jsx)(n.h3,{id:"reading-entities",children:"Reading entities"}),"\n",(0,r.jsxs)(n.p,{children:["Event handlers are a good place to make decisions and, to make better decisions, you need information. The ",(0,r.jsx)(n.code,{children:"Booster.entity"})," function allows you to inspect the application state. This function receives two arguments, the ",(0,r.jsx)(n.code,{children:"Entity"}),"'s name to fetch and the ",(0,r.jsx)(n.code,{children:"entityID"}),". Here is an example of fetching an entity called ",(0,r.jsx)(n.code,{children:"Stock"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/move-stock.ts"',children:"@Command()\nexport class MoveStock {\n public constructor(\n readonly productID: string,\n readonly origin: string,\n readonly destination: string,\n readonly quantity: number\n ) {}\n\n public static async handle(command: MoveStock, register: Register): Promise {\n // highlight-next-line\n const stock = await Booster.entity(Stock, command.productID)\n if (!command.enoughStock(command.origin, command.quantity, stock)) {\n register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))\n }\n }\n\n private enoughStock(origin: string, quantity: number, stock?: Stock): boolean {\n const count = stock?.countByLocation[origin]\n return !!count && count >= quantity\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"authorizing-a-command",children:"Authorizing a command"}),"\n",(0,r.jsxs)(n.p,{children:["Commands are part of the public API of a Booster application, so you can define who is authorized to submit them. All commands are protected by default, which means that no one can submit them. In order to allow users to submit a command, you must explicitly authorize them. You can use the ",(0,r.jsx)(n.code,{children:"authorize"})," field of the ",(0,r.jsx)(n.code,{children:"@Command"})," decorator to specify the authorization rule."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command({\n // highlight-next-line\n authorize: 'all',\n})\nexport class CreateProduct {\n public constructor(\n readonly sku: Sku,\n readonly displayName: string,\n readonly description: string,\n readonly price: number\n ) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n register.events(/* YOUR EVENT HERE */)\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["You can read more about this on the ",(0,r.jsx)(n.a,{href:"/security/authorization",children:"Authorization section"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"submitting-a-command",children:"Submitting a command"}),"\n",(0,r.jsx)(n.p,{children:"Booster commands are accessible to the outside world as GraphQL mutations. GrahpQL fits very well with Booster's CQRS approach because it has two kinds of operations: Mutations and Queries. Mutations are actions that modify the server-side data, just like commands."}),"\n",(0,r.jsxs)(n.p,{children:["Booster automatically creates one mutation per command. The framework infers the mutation input type from the command fields. Given this ",(0,r.jsx)(n.code,{children:"CreateProduct"})," command:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"@Command({\n authorize: 'all',\n})\nexport class CreateProduct {\n public constructor(\n readonly sku: Sku,\n readonly displayName: string,\n readonly description: string,\n readonly price: number\n ) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n register.events(/* YOUR EVENT HERE */)\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"Booster generates the following GraphQL mutation:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-graphql",children:"mutation CreateProduct($input: CreateProductInput!): Boolean\n"})}),"\n",(0,r.jsxs)(n.p,{children:["where the schema for ",(0,r.jsx)(n.code,{children:"CreateProductInput"})," is"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"{\n sku: String\n displayName: String\n description: String\n price: Float\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"commands-naming-convention",children:"Commands naming convention"}),"\n",(0,r.jsxs)(n.p,{children:["Semantics are very important in Booster as it will play an essential role in designing a coherent system. Your application should reflect your domain concepts, and commands are not an exception. Although you can name commands in any way you want, we strongly recommend you to ",(0,r.jsx)(n.strong,{children:"name them starting with verbs in imperative plus the object being affected"}),". If we were designing an e-commerce application, some commands would be:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"CreateProduct"}),"\n",(0,r.jsx)(n.li,{children:"DeleteProduct"}),"\n",(0,r.jsx)(n.li,{children:"UpdateProduct"}),"\n",(0,r.jsx)(n.li,{children:"ChangeCartItems"}),"\n",(0,r.jsx)(n.li,{children:"ConfirmPayment"}),"\n",(0,r.jsx)(n.li,{children:"MoveStock"}),"\n",(0,r.jsx)(n.li,{children:"UpdateCartShippingAddress"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Despite you can place commands, and other Booster files, in any directory, we strongly recommend you to put them in ",(0,r.jsx)(n.code,{children:"/src/commands"}),". Having all the commands in one place will help you to understand your application's capabilities at a glance."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"\n\u251c\u2500\u2500 src\n\u2502\xa0\xa0 \u251c\u2500\u2500 commands <------ put them here\n\u2502\xa0\xa0 \u251c\u2500\u2500 common\n\u2502\xa0\xa0 \u251c\u2500\u2500 config\n\u2502\xa0\xa0 \u251c\u2500\u2500 entities\n\u2502\xa0\xa0 \u251c\u2500\u2500 events\n\u2502\xa0\xa0 \u251c\u2500\u2500 index.ts\n\u2502\xa0\xa0 \u2514\u2500\u2500 read-models\n"})})]})}function h(e={}){const{wrapper:n}={...(0,a.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(m,{...e})}):m(e)}},5163:(e,n,t)=>{t.d(n,{Z:()=>o});t(7294);const r={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var a=t(5893);function o(e){let{children:n}=e;return(0,a.jsxs)("div",{className:r.terminalWindow,children:[(0,a.jsx)("div",{className:r.terminalWindowHeader,children:(0,a.jsxs)("div",{className:r.buttons,children:[(0,a.jsx)("span",{className:r.dot,style:{background:"#f25f58"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#fbbe3c"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#58cb42"}})]})}),(0,a.jsx)("div",{className:r.terminalWindowBody,children:n})]})}},1151:(e,n,t)=>{t.d(n,{Z:()=>s,a:()=>i});var r=t(7294);const a={},o=r.createContext(a);function i(e){const n=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:i(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/5b078add.f1615d64.js b/assets/js/5b078add.f1615d64.js new file mode 100644 index 000000000..61e9c5b4c --- /dev/null +++ b/assets/js/5b078add.f1615d64.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[1422],{4074:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>s,default:()=>h,frontMatter:()=>i,metadata:()=>c,toc:()=>l});var r=t(5893),a=t(1151),o=t(5163);const i={},s="Command",c={id:"architecture/command",title:"Command",description:"Commands are any action a user performs on your application. For example, RemoveItemFromCart, RatePhoto or AddCommentToPost. They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a request on a REST API. Command issuers can also send data on a command as parameters.",source:"@site/docs/03_architecture/02_command.mdx",sourceDirName:"03_architecture",slug:"/architecture/command",permalink:"/architecture/command",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/03_architecture/02_command.mdx",tags:[],version:"current",lastUpdatedBy:"Nick Seagull",lastUpdatedAt:1706202233,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:2,frontMatter:{},sidebar:"docs",previous:{title:"Booster architecture",permalink:"/architecture/event-driven"},next:{title:"Event",permalink:"/architecture/event"}},d={},l=[{value:"Creating a command",id:"creating-a-command",level:2},{value:"Declaring a command",id:"declaring-a-command",level:2},{value:"The command handler function",id:"the-command-handler-function",level:2},{value:"Registering events",id:"registering-events",level:3},{value:"Returning a value",id:"returning-a-value",level:3},{value:"Validating data",id:"validating-data",level:3},{value:"Throw an error",id:"throw-an-error",level:4},{value:"Register error events",id:"register-error-events",level:4},{value:"Reading entities",id:"reading-entities",level:3},{value:"Authorizing a command",id:"authorizing-a-command",level:2},{value:"Submitting a command",id:"submitting-a-command",level:2},{value:"Commands naming convention",id:"commands-naming-convention",level:2}];function m(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"command",children:"Command"}),"\n",(0,r.jsxs)(n.p,{children:["Commands are any action a user performs on your application. For example, ",(0,r.jsx)(n.code,{children:"RemoveItemFromCart"}),", ",(0,r.jsx)(n.code,{children:"RatePhoto"})," or ",(0,r.jsx)(n.code,{children:"AddCommentToPost"}),". They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a ",(0,r.jsx)(n.strong,{children:"request on a REST API"}),". Command issuers can also send data on a command as parameters."]}),"\n",(0,r.jsx)(n.h2,{id:"creating-a-command",children:"Creating a command"}),"\n",(0,r.jsx)(n.p,{children:"The Booster CLI will help you to create new commands. You just need to run the following command and the CLI will generate all the boilerplate for you:"}),"\n",(0,r.jsx)(o.Z,{children:(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-shell",children:"boost new:command CreateProduct --fields sku:SKU displayName:string description:string price:Money\n"})})}),"\n",(0,r.jsxs)(n.p,{children:["This will generate a new file called ",(0,r.jsx)(n.code,{children:"create-product"})," in the ",(0,r.jsx)(n.code,{children:"src/commands"})," directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI."]}),"\n",(0,r.jsx)(n.h2,{id:"declaring-a-command",children:"Declaring a command"}),"\n",(0,r.jsxs)(n.p,{children:["In Booster you define them as TypeScript classes decorated with the ",(0,r.jsx)(n.code,{children:"@Command"})," decorator. The ",(0,r.jsx)(n.code,{children:"Command"})," parameters will be declared as properties of the class."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/command-name.ts"',children:"@Command()\nexport class CommandName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType) {}\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["These commands are handled by ",(0,r.jsx)(n.code,{children:"Command Handlers"}),", the same way a ",(0,r.jsx)(n.strong,{children:"REST Controller"})," do with a request. To create a ",(0,r.jsx)(n.code,{children:"Command handler"})," of a specific Command, you must declare a ",(0,r.jsx)(n.code,{children:"handle"})," class function inside the corresponding command you want to handle. For example:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/command-name.ts"',children:"@Command()\nexport class CommandName {\n public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType) {}\n\n // highlight-start\n public static async handle(command: CommandName, register: Register): Promise {\n // Validate inputs\n // Run domain logic\n // register.events([event1,...])\n }\n // highlight-end\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Booster will then generate the GraphQL mutation for the corresponding command, and the infrastructure to handle them. You only have to define the class and the handler function. Commands are part of the public API, so you can define authorization policies for them, you can read more about this on ",(0,r.jsx)(n.a,{href:"/security/authorization",children:"the authorization section"}),"."]}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsx)(n.p,{children:"We recommend using command handlers to validate input data before registering events into the event store because they are immutable once there."})}),"\n",(0,r.jsx)(n.h2,{id:"the-command-handler-function",children:"The command handler function"}),"\n",(0,r.jsxs)(n.p,{children:["Each command class must have a method called ",(0,r.jsx)(n.code,{children:"handle"}),". This function is the command handler, and it will be called by the framework every time one instance of this command is submitted. Inside the handler you can run validations, return errors, query entities to make decisions, and register relevant domain events."]}),"\n",(0,r.jsx)(n.h3,{id:"registering-events",children:"Registering events"}),"\n",(0,r.jsxs)(n.p,{children:["Within the command handler execution, it is possible to register domain events. The command handler function receives the ",(0,r.jsx)(n.code,{children:"register"})," argument, so within the handler, it is possible to call ",(0,r.jsx)(n.code,{children:"register.events(...)"})," with a list of events."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command()\nexport class CreateProduct {\n public constructor(readonly sku: string, readonly price: number) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n // highlight-next-line\n register.event(new ProductCreated(/*...*/))\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["For more details about events and the register parameter, see the ",(0,r.jsx)(n.a,{href:"/architecture/event",children:(0,r.jsx)(n.code,{children:"Events"})})," section."]}),"\n",(0,r.jsx)(n.h3,{id:"returning-a-value",children:"Returning a value"}),"\n",(0,r.jsxs)(n.p,{children:["The command handler function can return a value. This value will be the response of the GraphQL mutation. By default, the command handler function expects you to return a ",(0,r.jsx)(n.code,{children:"void"})," as a return type. Since GrahpQL does not have a ",(0,r.jsx)(n.code,{children:"void"})," type, the command handler function returns ",(0,r.jsx)(n.code,{children:"true"})," when called through the GraphQL. This is because the GraphQL specification requires a response, and ",(0,r.jsx)(n.code,{children:"true"})," is the most appropriate value to represent a successful execution with no return value."]}),"\n",(0,r.jsxs)(n.p,{children:["If you want to return a value, you can change the return type of the handler function. For example, if you want to return a ",(0,r.jsx)(n.code,{children:"string"}),":"]}),"\n",(0,r.jsx)(n.p,{children:"For example:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command()\nexport class CreateProduct {\n public constructor(readonly sku: string, readonly price: number) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n register.event(new ProductCreated(/*...*/))\n // highlight-next-line\n return 'Product created!'\n }\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"validating-data",children:"Validating data"}),"\n",(0,r.jsx)(n.admonition,{type:"tip",children:(0,r.jsxs)(n.p,{children:["Booster uses the typed nature of GraphQL to ensure that types are correct before reaching the handler, so ",(0,r.jsx)(n.strong,{children:"you don't have to validate types"}),"."]})}),"\n",(0,r.jsx)(n.h4,{id:"throw-an-error",children:"Throw an error"}),"\n",(0,r.jsx)(n.p,{children:"A command will fail if there is an uncaught error during its handling. When a command fails, Booster will return a detailed error response with the message of the thrown error. This is useful for debugging, but it is also a security feature. Booster will never return an error stack trace to the client, so you don't have to worry about exposing internal implementation details."}),"\n",(0,r.jsx)(n.p,{children:"One case where you might want to throw an error is when the command is invalid because it breaks a business rule. For example, if the command contains a negative price. In that case, you can throw an error in the handler. Booster will use the error's message as the response to make it descriptive. For example, given this command:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command()\nexport class CreateProduct {\n public constructor(readonly sku: string, readonly price: number) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n const priceLimit = 10\n if (command.price >= priceLimit) {\n // highlight-next-line\n throw new Error(`price must be below ${priceLimit}, and it was ${command.price}`)\n }\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"You'll get something like this response:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\n "errors": [\n {\n "message": "price must be below 10, and it was 19.99",\n "path": ["CreateProduct"]\n }\n ]\n}\n'})}),"\n",(0,r.jsx)(n.h4,{id:"register-error-events",children:"Register error events"}),"\n",(0,r.jsx)(n.p,{children:"There could be situations in which you want to register an event representing an error. For example, when moving items with insufficient stock from one location to another:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/move-stock.ts"',children:"@Command()\nexport class MoveStock {\n public constructor(\n readonly productID: string,\n readonly origin: string,\n readonly destination: string,\n readonly quantity: number\n ) {}\n\n public static async handle(command: MoveStock, register: Register): Promise {\n if (!command.enoughStock(command.productID, command.origin, command.quantity)) {\n // highlight-next-line\n register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))\n } else {\n register.events(new StockMoved(/*...*/))\n }\n }\n\n private enoughStock(productID: string, origin: string, quantity: number): boolean {\n /* ... */\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"In this case, the command operation can still be completed. An event handler will take care of that `ErrorEvent and proceed accordingly."}),"\n",(0,r.jsx)(n.h3,{id:"reading-entities",children:"Reading entities"}),"\n",(0,r.jsxs)(n.p,{children:["Event handlers are a good place to make decisions and, to make better decisions, you need information. The ",(0,r.jsx)(n.code,{children:"Booster.entity"})," function allows you to inspect the application state. This function receives two arguments, the ",(0,r.jsx)(n.code,{children:"Entity"}),"'s name to fetch and the ",(0,r.jsx)(n.code,{children:"entityID"}),". Here is an example of fetching an entity called ",(0,r.jsx)(n.code,{children:"Stock"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/move-stock.ts"',children:"@Command()\nexport class MoveStock {\n public constructor(\n readonly productID: string,\n readonly origin: string,\n readonly destination: string,\n readonly quantity: number\n ) {}\n\n public static async handle(command: MoveStock, register: Register): Promise {\n // highlight-next-line\n const stock = await Booster.entity(Stock, command.productID)\n if (!command.enoughStock(command.origin, command.quantity, stock)) {\n register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))\n }\n }\n\n private enoughStock(origin: string, quantity: number, stock?: Stock): boolean {\n const count = stock?.countByLocation[origin]\n return !!count && count >= quantity\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"authorizing-a-command",children:"Authorizing a command"}),"\n",(0,r.jsxs)(n.p,{children:["Commands are part of the public API of a Booster application, so you can define who is authorized to submit them. All commands are protected by default, which means that no one can submit them. In order to allow users to submit a command, you must explicitly authorize them. You can use the ",(0,r.jsx)(n.code,{children:"authorize"})," field of the ",(0,r.jsx)(n.code,{children:"@Command"})," decorator to specify the authorization rule."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="src/commands/create-product.ts"',children:"@Command({\n // highlight-next-line\n authorize: 'all',\n})\nexport class CreateProduct {\n public constructor(\n readonly sku: Sku,\n readonly displayName: string,\n readonly description: string,\n readonly price: number\n ) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n register.events(/* YOUR EVENT HERE */)\n }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["You can read more about this on the ",(0,r.jsx)(n.a,{href:"/security/authorization",children:"Authorization section"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"submitting-a-command",children:"Submitting a command"}),"\n",(0,r.jsx)(n.p,{children:"Booster commands are accessible to the outside world as GraphQL mutations. GrahpQL fits very well with Booster's CQRS approach because it has two kinds of operations: Mutations and Queries. Mutations are actions that modify the server-side data, just like commands."}),"\n",(0,r.jsxs)(n.p,{children:["Booster automatically creates one mutation per command. The framework infers the mutation input type from the command fields. Given this ",(0,r.jsx)(n.code,{children:"CreateProduct"})," command:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",children:"@Command({\n authorize: 'all',\n})\nexport class CreateProduct {\n public constructor(\n readonly sku: Sku,\n readonly displayName: string,\n readonly description: string,\n readonly price: number\n ) {}\n\n public static async handle(command: CreateProduct, register: Register): Promise {\n register.events(/* YOUR EVENT HERE */)\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"Booster generates the following GraphQL mutation:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-graphql",children:"mutation CreateProduct($input: CreateProductInput!): Boolean\n"})}),"\n",(0,r.jsxs)(n.p,{children:["where the schema for ",(0,r.jsx)(n.code,{children:"CreateProductInput"})," is"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"{\n sku: String\n displayName: String\n description: String\n price: Float\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"commands-naming-convention",children:"Commands naming convention"}),"\n",(0,r.jsxs)(n.p,{children:["Semantics are very important in Booster as it will play an essential role in designing a coherent system. Your application should reflect your domain concepts, and commands are not an exception. Although you can name commands in any way you want, we strongly recommend you to ",(0,r.jsx)(n.strong,{children:"name them starting with verbs in imperative plus the object being affected"}),". If we were designing an e-commerce application, some commands would be:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"CreateProduct"}),"\n",(0,r.jsx)(n.li,{children:"DeleteProduct"}),"\n",(0,r.jsx)(n.li,{children:"UpdateProduct"}),"\n",(0,r.jsx)(n.li,{children:"ChangeCartItems"}),"\n",(0,r.jsx)(n.li,{children:"ConfirmPayment"}),"\n",(0,r.jsx)(n.li,{children:"MoveStock"}),"\n",(0,r.jsx)(n.li,{children:"UpdateCartShippingAddress"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Despite you can place commands, and other Booster files, in any directory, we strongly recommend you to put them in ",(0,r.jsx)(n.code,{children:"/src/commands"}),". Having all the commands in one place will help you to understand your application's capabilities at a glance."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-text",children:"\n\u251c\u2500\u2500 src\n\u2502\xa0\xa0 \u251c\u2500\u2500 commands <------ put them here\n\u2502\xa0\xa0 \u251c\u2500\u2500 common\n\u2502\xa0\xa0 \u251c\u2500\u2500 config\n\u2502\xa0\xa0 \u251c\u2500\u2500 entities\n\u2502\xa0\xa0 \u251c\u2500\u2500 events\n\u2502\xa0\xa0 \u251c\u2500\u2500 index.ts\n\u2502\xa0\xa0 \u2514\u2500\u2500 read-models\n"})})]})}function h(e={}){const{wrapper:n}={...(0,a.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(m,{...e})}):m(e)}},5163:(e,n,t)=>{t.d(n,{Z:()=>o});t(7294);const r={terminalWindow:"terminalWindow_wGrl",terminalWindowHeader:"terminalWindowHeader_o9Cs",row:"row_Rn7G",buttons:"buttons_IGLB",right:"right_fWp9",terminalWindowAddressBar:"terminalWindowAddressBar_X8fO",dot:"dot_fGZE",terminalWindowMenuIcon:"terminalWindowMenuIcon_rtOE",bar:"bar_Ck8N",terminalWindowBody:"terminalWindowBody_tzdS"};var a=t(5893);function o(e){let{children:n}=e;return(0,a.jsxs)("div",{className:r.terminalWindow,children:[(0,a.jsx)("div",{className:r.terminalWindowHeader,children:(0,a.jsxs)("div",{className:r.buttons,children:[(0,a.jsx)("span",{className:r.dot,style:{background:"#f25f58"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#fbbe3c"}}),(0,a.jsx)("span",{className:r.dot,style:{background:"#58cb42"}})]})}),(0,a.jsx)("div",{className:r.terminalWindowBody,children:n})]})}},1151:(e,n,t)=>{t.d(n,{Z:()=>s,a:()=>i});var r=t(7294);const a={},o=r.createContext(a);function i(e){const n=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:i(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/5e911e87.8ce6cfff.js b/assets/js/5e911e87.8ce6cfff.js deleted file mode 100644 index 24f585726..000000000 --- a/assets/js/5e911e87.8ce6cfff.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[9089],{1549:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>a,toc:()=>c});var s=t(5893),i=t(1151);const r={},o="Contributing to Booster",a={id:"contributing",title:"Contributing to Booster",description:"DISCLAIMER: The Booster docs are undergoing an overhaul. Most of what's written here applies, but expect some hiccups in the build process",source:"@site/docs/12_contributing.md",sourceDirName:".",slug:"/contributing",permalink:"/contributing",draft:!1,unlisted:!1,editUrl:"https://github.com/boostercloud/booster/tree/main/website/docs/12_contributing.md",tags:[],version:"current",lastUpdatedBy:"Gonzalo Garcia Jaubert",lastUpdatedAt:1706192347,formattedLastUpdatedAt:"Jan 25, 2024",sidebarPosition:12,frontMatter:{},sidebar:"docs",previous:{title:"Frequently Asked Questions",permalink:"/frequently-asked-questions"}},l={},c=[{value:"Code of Conduct",id:"code-of-conduct",level:2},{value:"I don't want to read this whole thing, I just have a question",id:"i-dont-want-to-read-this-whole-thing-i-just-have-a-question",level:2},{value:"What should I know before I get started?",id:"what-should-i-know-before-i-get-started",level:2},{value:"Packages",id:"packages",level:3},{value:"How Can I Contribute?",id:"how-can-i-contribute",level:2},{value:"Reporting Bugs",id:"reporting-bugs",level:3},{value:"Suggesting Enhancements",id:"suggesting-enhancements",level:3},{value:"Improving documentation",id:"improving-documentation",level:3},{value:"Documentation principles and practices",id:"documentation-principles-and-practices",level:4},{value:"Principles",id:"principles",level:5},{value:"Practices",id:"practices",level:5},{value:"Create your very first GitHub issue",id:"create-your-very-first-github-issue",level:3},{value:"Your First Code Contribution",id:"your-first-code-contribution",level:2},{value:"Getting the code",id:"getting-the-code",level:3},{value:"Understanding the "rush monorepo" approach and how dependencies are structured in the project",id:"understanding-the-rush-monorepo-approach-and-how-dependencies-are-structured-in-the-project",level:3},{value:"Running unit tests",id:"running-unit-tests",level:3},{value:"Running integration tests",id:"running-integration-tests",level:3},{value:"Github flow",id:"github-flow",level:3},{value:"Publishing your Pull Request",id:"publishing-your-pull-request",level:3},{value:"Branch naming conventions",id:"branch-naming-conventions",level:3},{value:"Commit message guidelines",id:"commit-message-guidelines",level:3},{value:"Code Style Guidelines",id:"code-style-guidelines",level:2},{value:"Importing other files and libraries",id:"importing-other-files-and-libraries",level:3},{value:"Functional style",id:"functional-style",level:3},{value:"Use const and let",id:"use-const-and-let",level:3}];function d(e){const n={a:"a",blockquote:"blockquote",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",h4:"h4",h5:"h5",img:"img",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"contributing-to-booster",children:"Contributing to Booster"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"DISCLAIMER:"})," The Booster docs are undergoing an overhaul. Most of what's written here applies, but expect some hiccups in the build process\nthat is described here, as it changed in the last version. New documentation will have this documented properly."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Thanks for taking the time to contribute to Booster. It is an open-source project and it wouldn't be possible without people like you \ud83d\ude4f\ud83c\udf89"}),"\n",(0,s.jsxs)(n.p,{children:["This document is a set of guidelines to help you contribute to Booster, which is hosted on the ",(0,s.jsx)(n.a,{href:"https://github.com/boostercloud",children:(0,s.jsx)(n.code,{children:"boostercloud"})})," GitHub\norganization. These aren\u2019t absolute laws, use your judgment and common sense \ud83d\ude00.\nRemember that if something here doesn't make sense, you can also propose a change to this document."]}),"\n",(0,s.jsx)(n.h2,{id:"code-of-conduct",children:"Code of Conduct"}),"\n",(0,s.jsxs)(n.p,{children:["This project and everyone participating in it are expected to uphold the ",(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/booster/blob/main/CODE_OF_CONDUCT.md",children:"Booster's Code of Conduct"}),", based on the Covenant Code of Conduct.\nIf you see unacceptable behavior, please communicate so to ",(0,s.jsx)(n.code,{children:"hello@booster.cloud"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"i-dont-want-to-read-this-whole-thing-i-just-have-a-question",children:"I don't want to read this whole thing, I just have a question"}),"\n",(0,s.jsxs)(n.p,{children:["Go ahead and ask the community in ",(0,s.jsx)(n.a,{href:"https://discord.com/invite/bDY8MKx",children:"Discord"})," or ",(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/booster/issues",children:"create a new issue"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"what-should-i-know-before-i-get-started",children:"What should I know before I get started?"}),"\n",(0,s.jsx)(n.h3,{id:"packages",children:"Packages"}),"\n",(0,s.jsx)(n.p,{children:"Booster is divided in many different packages. The criteria to split the code in packages is that each package meets at least one of the following conditions:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"They must be run separately, for instance, the CLI is run locally, while the support code for the project is run on the cloud."}),"\n",(0,s.jsx)(n.li,{children:"They contain code that is used by at least two of the other packages."}),"\n",(0,s.jsx)(n.li,{children:"They're a vendor-specific specialization of some abstract part of the framework (for instance, all the code that is required by Azure is in separate packages)."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The packages are managed using ",(0,s.jsx)(n.a,{href:"https://rushjs.io/",children:"rush"})," and ",(0,s.jsx)(n.a,{href:"https://npmjs.com",children:"npm"}),", if you run ",(0,s.jsx)(n.code,{children:"rush build"}),", it will build all the packages."]}),"\n",(0,s.jsxs)(n.p,{children:["The packages are published to ",(0,s.jsx)(n.code,{children:"npmjs"})," under the prefix ",(0,s.jsx)(n.code,{children:"@boostercloud/"}),", their purpose is as follows:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"cli"})," - You guessed it! This package is the ",(0,s.jsx)(n.code,{children:"boost"})," command-line tool, it interacts only with the core package in order to load the project configuration. The specific provider packages to interact with the cloud providers are loaded dynamically from the project config."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-core"})," - This one contains all the framework runtime vendor-independent logic. Stuff like the generation of the config or the commands and events handling happens here. The specific provider packages to interact with the cloud providers are loaded dynamically from the project config."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-integration-tests"})," - Implements integration tests for all supported vendors. Tests are run on real infrastructure using the same mechanisms than a production application. This package ",(0,s.jsx)(n.code,{children:"src"})," folder includes a synthetic Booster application that can be deployed to a real provider for testing purposes."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-provider-aws"})," (Currently Deprecated) - Implements all the required adapters to make the booster core run on top of AWS technologies like Lambda and DynamoDB using the AWS SDK under the hoods."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-provider-aws-infrastructure"})," (Currently Deprecated) - Implements all the required adapters to allow Booster applications to be deployed to AWS using the AWS CDK under the hoods."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-provider-local"})," - Implements all the required adapters to run the Booster application on a local express server to be able to debug your code before deploying it to a real cloud provider."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-provider-local-infrastructure"})," - Implements all the required code to run the local development server."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"framework-types"})," - This package defines types that the rest of the project will use. This is useful for avoiding cyclic dependencies. Note that this package should not contain stuff that are not types, or very simple methods related directly to them, i.e. a getter or setter. This package defines the main booster concepts like:","\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Entity"}),"\n",(0,s.jsx)(n.li,{children:"Command"}),"\n",(0,s.jsx)(n.li,{children:"etc\u2026"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["This is a dependency graph that shows the dependencies among all packages, including the application using Booster:\n",(0,s.jsx)(n.img,{src:"https://raw.githubusercontent.com/boostercloud/booster/main/docs/img/packages-dependencies.png",alt:"Booster packages dependencies"})]}),"\n",(0,s.jsx)(n.h2,{id:"how-can-i-contribute",children:"How Can I Contribute?"}),"\n",(0,s.jsx)(n.p,{children:"Contributing to an open source project is never just a matter of code, you can help us significantly by just using Booster and interacting with our community. Here you'll find some tips on how to do it effectively."}),"\n",(0,s.jsx)(n.h3,{id:"reporting-bugs",children:"Reporting Bugs"}),"\n",(0,s.jsx)(n.p,{children:"Before creating a bug report, please search for similar issues to make sure that they're not already reported. If you don't find any, go ahead and create an issue including as many details as possible. Fill out the required template, the information requested helps us to resolve issues faster."}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Note"}),": If you find a Closed issue that seems related to the issues that you're experiencing, make sure to reference it in the body of your new one by writing its number like this => #42 (Github will autolink it for you)."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Bugs are tracked as GitHub issues. Explain the problem and include additional details to help maintainers reproduce the problem:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Use a clear and descriptive title for the issue to identify the problem."}),"\n",(0,s.jsx)(n.li,{children:"Describe the exact steps which reproduce the problem in as many details as possible."}),"\n",(0,s.jsx)(n.li,{children:"Provide specific examples to demonstrate the steps. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use Markdown code blocks."}),"\n",(0,s.jsx)(n.li,{children:"Describe the behavior you observed after following the steps and point out what exactly is the problem with that behavior."}),"\n",(0,s.jsx)(n.li,{children:"Explain which behavior you expected to see instead and why."}),"\n",(0,s.jsx)(n.li,{children:"If the problem is related to performance or memory, include a CPU profile capture with your report."}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"suggesting-enhancements",children:"Suggesting Enhancements"}),"\n",(0,s.jsx)(n.p,{children:"Enhancement suggestions are tracked as GitHub issues. Make sure you provide the following information:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Use a clear and descriptive title for the issue to identify the suggestion."}),"\n",(0,s.jsx)(n.li,{children:"Provide a step-by-step description of the suggested enhancement in as many details as possible."}),"\n",(0,s.jsx)(n.li,{children:"Provide specific examples to demonstrate the steps. Include copy/pasteable snippets which you use in those examples, as Markdown code blocks."}),"\n",(0,s.jsx)(n.li,{children:"Describe the current behavior and explain which behavior you expected to see instead and why."}),"\n",(0,s.jsx)(n.li,{children:"Explain why this enhancement would be useful to most Booster users and isn't something that can or should be implemented as a community package."}),"\n",(0,s.jsx)(n.li,{children:"List some other libraries or frameworks where this enhancement exists."}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"improving-documentation",children:"Improving documentation"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://docs.boosterframework.com",children:"Booster documentation"}),' is treated as a live document that continues improving on a daily basis. If you find something that is missing or can be improved, please contribute, it will be of great help for other developers.\nTo contribute you can use the button "Edit on github" at the top of each chapter.']}),"\n",(0,s.jsx)(n.h4,{id:"documentation-principles-and-practices",children:"Documentation principles and practices"}),"\n",(0,s.jsx)(n.p,{children:"The ultimate goal of a technical document is to translate the knowledge from the technology creators into the reader's mind so that they learn. The challenging\npart here is the one in which they learn. It is challenging because, under the same amount of information, a person can suffer an information overload because\nwe (humans) don't have the same information-processing capacity. That idea is going to work as our compass, it should drive our efforts so people with less\ncapacity is still able to follow and understand our documentation."}),"\n",(0,s.jsx)(n.p,{children:"To achieve our goal we propose writing documentation following these principles:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsx)(n.li,{children:"Clean and Clear"}),"\n",(0,s.jsx)(n.li,{children:"Simple"}),"\n",(0,s.jsx)(n.li,{children:"Coherent"}),"\n",(0,s.jsx)(n.li,{children:"Explicit"}),"\n",(0,s.jsx)(n.li,{children:"Attractive"}),"\n",(0,s.jsx)(n.li,{children:"Inclusive"}),"\n",(0,s.jsx)(n.li,{children:"Cohesive"}),"\n"]}),"\n",(0,s.jsx)(n.h5,{id:"principles",children:"Principles"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"1. Clean and Clear"})}),"\n",(0,s.jsxs)(n.p,{children:["Less is more. Apple is, among many others, a good example of creating clean and clear content, where visual elements are carefully chosen to look beautiful\n(e.g. ",(0,s.jsx)(n.a,{href:"https://developer.apple.com/tutorials/swiftui",children:"Apple's swift UI"}),") and making the reader getting the point as soon as possible."]}),"\n",(0,s.jsx)(n.p,{children:"The intention of every section, paragraph, and sentence must be clear, we should avoid writing details of two different things even when they are related.\nIt is better to link pages and keep the focus and the intention clear, Wikipedia is the best example on this."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"2. Simple"})}),"\n",(0,s.jsx)(n.p,{children:"Technical writings deal with different backgrounds and expertise from the readers. We should not assume the reader knows everything we are talking about\nbut we should not explain everything in the same paragraph or section. Every section has a goal to stick to the goal and link to internal or external resources\nto go deeper."}),"\n",(0,s.jsx)(n.p,{children:"Diagrams are great tools, you know a picture is worth more than a thousand words unless that picture contains too much information.\nKeep it simple intentionally omitting details."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"3. Coherent"})}),"\n",(0,s.jsx)(n.p,{children:"The documentation tells a story. Every section should integrate naturally without making the reader switch between different contexts. Text, diagrams,\nand code examples should support each other without introducing abrupt changes breaking the reader\u2019s flow. Also, the font, colors, diagrams, code samples,\nanimations, and all the visual elements we include, should support the story we are telling."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"4. Explicit"})}),"\n",(0,s.jsx)(n.p,{children:"Go straight to the point without assuming the readers should know about something. Again, link internal or external resources to clarify."}),"\n",(0,s.jsx)(n.p,{children:"The index of the whole content must be visible all the time so the reader knows exactly where they are and what is left."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"5. Attractive"})}),"\n",(0,s.jsx)(n.p,{children:"Our text must be nice to read, our diagrams delectable to see, and our site\u2026 a feast for the eyes!!"}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"6. Inclusive"})}),"\n",(0,s.jsx)(n.p,{children:"Everybody should understand our writings, especially the topics at the top. We have arranged the documentation structure in a way that anybody can dig\ndeeper by just going down so, sections 1 to 4 must be suitable for all ages."}),"\n",(0,s.jsx)(n.p,{children:"Use gender-neutral language to avoid the use of he, him, his to refer to undetermined gender. It is better to use their or they as a gender-neutral\napproach than s/he or similars."}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"7. Cohesive"})}),"\n",(0,s.jsx)(n.p,{children:"Writing short and concise sentences is good, but remember to use proper connectors (\u201cTherefore\u201d, \u201cBesides\u201d, \u201cHowever\u201d, \u201cthus\u201d, etc) that provide a\nsense of continuation to the whole paragraph. If not, when people read the paragraphs, their internal voice sounds like a robot with unnatural stops."}),"\n",(0,s.jsx)(n.p,{children:"For example, read this paragraph and try to hear your internal voice:"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Entities are created on the fly, by reducing the whole event stream. You shouldn't assume that they are stored anywhere. Booster does create\nautomatic snapshots to make the reduction process efficient. You are the one in charge of writing the reducer function."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"And now read this one:"}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsx)(n.p,{children:"Entities are created on the fly by reducing the whole event stream. While you shouldn't assume that they are stored anywhere, Booster does create automatic\nsnapshots to make the reduction process efficient. In any case, this is opaque to you and the only thing you should care is to provide the reducer function."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Did you feel the difference? The latter makes you feel that everything is connected, it is more cohesive."}),"\n",(0,s.jsx)(n.h5,{id:"practices",children:"Practices"}),"\n",(0,s.jsx)(n.p,{children:"There are many writing styles depending on the type of document. It is common within technical and scientific writing to use Inductive and/or Deductive styles\nfor paragraphs. They have different outcomes and one style may suit better in one case or another, that is why it is important to know them, and decide which\none to use in every moment. Let\u2019s see the difference with 2 recursive examples."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Deductive paragraphs ease the reading for advanced users but still allows you to elaborate on ideas and concepts for newcomers"}),". In deductive paragraphs,\nthe conclusions or definitions appear at the beginning, and then, details, facts, or supporting phrases complete the paragraph\u2019s idea. By placing the\nconclusion in the first sentence, the reader immediately identifies the main point so they can decide to skip the whole paragraph or keep reading.\nIf you take a look at the structure of this paragraph, it is deductive."]}),"\n",(0,s.jsxs)(n.p,{children:["On the other hand, if you want to drive the readers' attention and play with it as if they were in a roller coaster, you can do so by using a different approach.\nIn that approach, you first introduce the facts and ideas and then you wrap them with a conclusion. This style is more narrative and forces the reader to\ncontinue because the main idea is diluted in the whole paragraph. Once all the ideas are placed together, you can finally conclude the paragraph. ",(0,s.jsx)(n.strong,{children:"This style is\ncalled Inductive."})]}),"\n",(0,s.jsx)(n.p,{children:"The first paragraph is deductive and the last one is inductive. In general, it is better to use the deductive style, but if we stick to one, our writing will start looking weird and maybe boring.\nSo decide one or another being conscious about your intention."}),"\n",(0,s.jsx)(n.h3,{id:"create-your-very-first-github-issue",children:"Create your very first GitHub issue"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.a,{href:"https://github.com/boostercloud/booster/issues/new",children:"Click here"})," to start making contributions to Booster."]}),"\n",(0,s.jsx)(n.h2,{id:"your-first-code-contribution",children:"Your First Code Contribution"}),"\n",(0,s.jsxs)(n.p,{children:["Unsure where to begin contributing to Booster? You can start by looking through issued tagged as ",(0,s.jsx)(n.code,{children:"good-first-issue"})," and ",(0,s.jsx)(n.code,{children:"help-wanted"}),":"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Beginner issues - issues which should only require a few lines of code, and a test or two."}),"\n",(0,s.jsx)(n.li,{children:"Help wanted issues - issues which should be a bit more involved than beginner issues."}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Both issue lists are sorted by the total number of comments. While not perfect, number of comments is a reasonable proxy for impact a given change will have."}),"\n",(0,s.jsx)(n.p,{children:"Make sure that you assign the chosen issue to yourself to communicate your intention to work on it and reduce the possibilities of other people taking the same assignment."}),"\n",(0,s.jsx)(n.h3,{id:"getting-the-code",children:"Getting the code"}),"\n",(0,s.jsx)(n.p,{children:"To start contributing to the project you would need to set up the project in your system, to do so, you must first follow these steps in your terminal."}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Install Rush: ",(0,s.jsx)(n.code,{children:"npm install -g @microsoft/rush"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Clone the repo and get into the directory of the project: ",(0,s.jsx)(n.code,{children:"git clone && cd booster"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Install project dependencies: ",(0,s.jsx)(n.code,{children:"rush update"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Compile the project ",(0,s.jsx)(n.code,{children:"rush build"})]}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsx)(n.p,{children:"Add your contribution"}),"\n"]}),"\n",(0,s.jsxs)(n.li,{children:["\n",(0,s.jsxs)(n.p,{children:["Make sure everything works by ",(0,s.jsx)(n.a,{href:"#running-unit-tests",children:"executing the unit tests"}),": ",(0,s.jsx)(n.code,{children:"rush rest"})]}),"\n"]}),"\n"]}),"\n",(0,s.jsxs)(n.blockquote,{children:["\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"DISCLAIMER"}),": The integration test process changed, feel free to chime in into our Discord for more info"]}),"\n"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Make sure everything works by ",(0,s.jsx)(n.a,{href:"#running-integration-tests",children:"running the integration tests"}),":"]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"rush pack-integration-deps\ncd packages/framework-integration-tests\nrushx integration -v\n"})}),"\n",(0,s.jsx)(n.h3,{id:"understanding-the-rush-monorepo-approach-and-how-dependencies-are-structured-in-the-project",children:'Understanding the "rush monorepo" approach and how dependencies are structured in the project'}),"\n",(0,s.jsxs)(n.p,{children:["The Booster Framework project is organized following the ",(0,s.jsx)(n.a,{href:"https://rushjs.io/",children:'"rush monorepo"'}),' structure. There are several "package.json" files and each one has its purpose with regard to the dependencies you include on them:']}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:'The "package.json" files that are on each package root should contain the dependencies used by that specific package. Be sure to correctly differentiate which dependency is only for development and which one is for production.'}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Finally, ",(0,s.jsx)(n.strong,{children:"always use exact numbers for dependency versions"}),'. This means that if you want to add the dependency "graphql" in version 1.2.3, you should add ',(0,s.jsx)(n.code,{children:'"graphql": "1.2.3"'}),' to the corresponding "package.json" file, and never ',(0,s.jsx)(n.code,{children:'"graphql": "^1.2.3"'})," or ",(0,s.jsx)(n.code,{children:'"graphql": "~1.2.3"'}),". This restriction comes from hard problems we've had in the past."]}),"\n",(0,s.jsx)(n.h3,{id:"running-unit-tests",children:"Running unit tests"}),"\n",(0,s.jsxs)(n.p,{children:["Unit tests are executed when you type ",(0,s.jsx)(n.code,{children:"rush test"}),". If you want to run the unit tests for an especific package, you should move to the corresponding folder and run one of the following commands:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:cli -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"cli"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:core -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-core"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:provider-aws -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-provider-aws"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:provider-aws-infrastructure -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-provider-aws-infrastructure"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:provider-azure -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-provider-azure"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:provider-azure-infrastructure -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-provider-azure-infrastructure"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:provider-local -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-provider-local"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:provider-local-infrastructure -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-provider-local-infrastructure"})," package."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"rushx test:types -v"}),": Run unit tests for the ",(0,s.jsx)(n.code,{children:"framework-types"})," package."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"running-integration-tests",children:"Running integration tests"}),"\n",(0,s.jsxs)(n.p,{children:["Integration tests are run automatically in Github Actions when a PR is locked, but it would be recommendable to run them locally before submitting a PR for review. You can find several scripts in ",(0,s.jsx)(n.code,{children:"packages/framework-integration-tests/package.json"})," to run different test suites. You can run them using rush tool:"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.code,{children:"rushx - - + + +

    BEEP 0 - Index of Booster Evolution Enhancement Proposals

    · One min read
    Nick Tchayka
    STATUS - DRAFT

    This BEEP will contain the index of all Booster Evolution Enhancement Proposals, known as BEEPs. BEEP numbers are assigned by the BEEP editors, and once assigned are never changed.

    In the future, the BEEPs will be listed here by category. For now, use the sidebar on the left.

    -

    A good starting point is BEEP 1, which describes the BEEP process itself.

    +

    A good starting point is BEEP 1, which describes the BEEP process itself.

    \ No newline at end of file diff --git a/blog/0001-purpose-and-guidelines/index.html b/blog/0001-purpose-and-guidelines/index.html index d62113d23..6a1ab4f24 100644 --- a/blog/0001-purpose-and-guidelines/index.html +++ b/blog/0001-purpose-and-guidelines/index.html @@ -3,16 +3,16 @@ -BEEP 1 - Purpose and Guidelines | Booster Framework +BEEP 1 - Purpose and Guidelines | Booster Framework - - - + + + +

    Please state explicitly whether you believe that the proposal should be accepted into Booster.

    \ No newline at end of file diff --git a/blog/0002-project-target/index.html b/blog/0002-project-target/index.html index 66c321456..4d2d79037 100644 --- a/blog/0002-project-target/index.html +++ b/blog/0002-project-target/index.html @@ -3,16 +3,16 @@ -BEEP 2 - Target and User Persona | Booster Framework +BEEP 2 - Target and User Persona | Booster Framework - - - + + +

    BEEP 2 - Target and User Persona

    · 3 min read
    Nick Tchayka
    STATUS - ACCEPTED
    @@ -93,6 +93,6 @@

    Conclusion

    -

    This document outlines the dual focus of the Booster Framework's target audience and user personas. Our development and design efforts are directed towards supporting both the enterprise developers transitioning from traditional technologies to TypeScript, and the growing community of hobbyist developers seeking an accessible and easy-to-use framework for personal projects. This approach ensures the Booster Framework remains versatile and relevant across different scales and scopes of software development.

    +

    This document outlines the dual focus of the Booster Framework's target audience and user personas. Our development and design efforts are directed towards supporting both the enterprise developers transitioning from traditional technologies to TypeScript, and the growing community of hobbyist developers seeking an accessible and easy-to-use framework for personal projects. This approach ensures the Booster Framework remains versatile and relevant across different scales and scopes of software development.

    \ No newline at end of file diff --git a/blog/0003-principles-of-design/index.html b/blog/0003-principles-of-design/index.html index 749ea4e5e..8b4d0cbb6 100644 --- a/blog/0003-principles-of-design/index.html +++ b/blog/0003-principles-of-design/index.html @@ -3,16 +3,16 @@ -BEEP 3 - Principles of Design | Booster Framework +BEEP 3 - Principles of Design | Booster Framework - - - + + +

    BEEP 3 - Principles of Design

    · 4 min read
    Nick Tchayka
    STATUS - ACCEPTED
    @@ -50,6 +50,6 @@

    ExamplesConclusion

    -

    The design principles outlined for the Booster Framework serve as a guiding light for the project's development and implementation processes. The Principle of Least Astonishment ensures system behavior aligns with user expectations, enhancing usability. The Principle of Developer Happiness focuses on creating a fulfilling environment for developers, while the Principle of Least Effort promotes simplicity and efficiency. These principles collectively ensure that the Booster Framework remains attuned to the needs of its diverse user base, from enterprise developers to hobbyist programmers, ensuring a user-centric, efficient, and developer-friendly journey throughout its development.

    +

    The design principles outlined for the Booster Framework serve as a guiding light for the project's development and implementation processes. The Principle of Least Astonishment ensures system behavior aligns with user expectations, enhancing usability. The Principle of Developer Happiness focuses on creating a fulfilling environment for developers, while the Principle of Least Effort promotes simplicity and efficiency. These principles collectively ensure that the Booster Framework remains attuned to the needs of its diverse user base, from enterprise developers to hobbyist programmers, ensuring a user-centric, efficient, and developer-friendly journey throughout its development.

    \ No newline at end of file diff --git a/blog/0004-semantic-versioning/index.html b/blog/0004-semantic-versioning/index.html index dec6574f7..cbf3c21d0 100644 --- a/blog/0004-semantic-versioning/index.html +++ b/blog/0004-semantic-versioning/index.html @@ -3,16 +3,16 @@ -BEEP 4 - Semantic Versioning | Booster Framework +BEEP 4 - Semantic Versioning | Booster Framework - - - + + +

    BEEP 4 - Semantic Versioning

    · 3 min read
    Nick Tchayka
    STATUS - ACCEPTED
    @@ -29,6 +29,6 @@

    Standardizing the Use of SemVer

    We advise using the Semantic Versioning schema as a standard across all Booster Framework projects. This uniformity will ensure coherence within the ecosystem. The Booster CLI tool should facilitate version management by inspecting exported functions and types, suggesting the next version based on the changes made.

    Considerations

    -

    It's important to note that changes in the implementation of a function, even without altering the API, can produce different results and are considered breaking changes. These should be reflected in the version number accordingly. For more information and examples, please refer to this GitHub thread.

    +

    It's important to note that changes in the implementation of a function, even without altering the API, can produce different results and are considered breaking changes. These should be reflected in the version number accordingly. For more information and examples, please refer to this GitHub thread.

    \ No newline at end of file diff --git a/blog/0005-agent-codebase/index.html b/blog/0005-agent-codebase/index.html index 89593e6df..c51241081 100644 --- a/blog/0005-agent-codebase/index.html +++ b/blog/0005-agent-codebase/index.html @@ -3,16 +3,16 @@ -BEEP 5 - Agent-Based Codebase Structure | Booster Framework +BEEP 5 - Agent-Based Codebase Structure | Booster Framework - - - + + +

    BEEP 5 - Agent-Based Codebase Structure

    · 4 min read
    Nick Tchayka
    STATUS - DRAFT
    @@ -102,6 +102,6 @@

    Int

    Each AI agent would operate within its designated subfolder, ensuring a clear delineation and manageable interaction with regular agents, while adhering to the same communication protocols and interface standards established in the overall framework.

    This innovative approach opens new avenues for efficiency, creativity, and advanced functionalities within the Booster Framework ecosystem.

    Conclusion

    -

    The proposed semantic codebase structure for Booster Framework aims to address current challenges in codebase management while aligning with the principles of modularity and scalability. This reorganization, centered around the concept of "Agents", offers numerous benefits, including improved code separation, easier collaboration, and the potential for scalable codebase management.

    +

    The proposed semantic codebase structure for Booster Framework aims to address current challenges in codebase management while aligning with the principles of modularity and scalability. This reorganization, centered around the concept of "Agents", offers numerous benefits, including improved code separation, easier collaboration, and the potential for scalable codebase management.

    \ No newline at end of file diff --git a/blog/0006-remote-imports/index.html b/blog/0006-remote-imports/index.html index fa1fc7b25..7b6dcf0c9 100644 --- a/blog/0006-remote-imports/index.html +++ b/blog/0006-remote-imports/index.html @@ -3,16 +3,16 @@ -BEEP 6 - Remote Imports | Booster Framework +BEEP 6 - Remote Imports | Booster Framework - - - + + +

    BEEP 6 - Remote Imports

    · 3 min read
    Nick Tchayka
    STATUS - DRAFT
    @@ -50,6 +50,6 @@

    Agent Naming Guidelines: Specific guidelines and constraints for naming agents will be established to ensure clarity and avoid conflicts in the import process.

    Conclusion

    -

    The introduction of the /inspect endpoint and the extension of the TypeScript compiler with a custom plugin represent a significant step forward in addressing the challenges of remote imports in Booster Framework. This solution not only simplifies the development process but also enhances the modularity and flexibility of the framework, ensuring a smooth and efficient experience for developers working across different agents and repositories.

    +

    The introduction of the /inspect endpoint and the extension of the TypeScript compiler with a custom plugin represent a significant step forward in addressing the challenges of remote imports in Booster Framework. This solution not only simplifies the development process but also enhances the modularity and flexibility of the framework, ensuring a smooth and efficient experience for developers working across different agents and repositories.

    \ No newline at end of file diff --git a/blog/archive/index.html b/blog/archive/index.html index fde99cb25..55e789cee 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -3,16 +3,16 @@ -Archive | Booster Framework +Archive | Booster Framework - - - + + + diff --git a/blog/atom.xml b/blog/atom.xml index 080575ca3..30361a144 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -1,22 +1,22 @@ - https://boosterframework.com/blog + https://docs.boosterframework.com/blog Booster Framework Blog 2024-01-24T00:00:00.000Z https://github.com/jpmonette/feed - + Booster Framework Blog - https://boosterframework.com/img/favicon.png + https://docs.boosterframework.com/img/favicon.png <![CDATA[BEEP 0 - Index of Booster Evolution Enhancement Proposals]]> - https://boosterframework.com/blog/0000-index - + https://docs.boosterframework.com/blog/0000-index + 2024-01-24T00:00:00.000Z
    STATUS - DRAFT

    This BEEP will contain the index of all Booster Evolution Enhancement Proposals, known as BEEPs. BEEP numbers are assigned by the BEEP editors, and once assigned are never changed.

    In the future, the BEEPs will be listed here by category. For now, use the sidebar on the left.

    -

    A good starting point is BEEP 1, which describes the BEEP process itself.

    ]]>
    +

    A good starting point is BEEP 1, which describes the BEEP process itself.

    ]]> Nick Tchayka https://github.com/NickSeagull @@ -24,21 +24,21 @@
    <![CDATA[BEEP 1 - Purpose and Guidelines]]> - https://boosterframework.com/blog/0001-purpose-and-guidelines - + https://docs.boosterframework.com/blog/0001-purpose-and-guidelines + 2024-01-24T00:01:00.000Z
    STATUS - IN PROGRESS
    -

    What is a BEEP?

    +

    What is a BEEP?

    BEEP stands for Booster Evolution Enhancement Proposal. It is a document that describes a change or addition to Booster.

    -

    Statuses of a BEEP

    +

    Statuses of a BEEP

    A BEEP can have one of the following statuses:

    STATUS - DRAFT

    This status indicates that the BEEP is still being written and is not ready for review.

    STATUS - IN PROGRESS

    This status indicates that the BEEP has been accepted but is still being implemented.

    STATUS - INTEGRATED

    This status indicates that the BEEP has been implemented.

    STATUS - ACCEPTED

    This status is for informational BEEPs that have been accepted.

    STATUS - REJECTED

    This status indicates that the BEEP has been rejected.

    -

    How to contribute to the design process

    +

    How to contribute to the design process

    Everyone is welcome to propose, discuss, and review ideas to improve Booster in the #proposals channel of the Discord server.

    Note that the project is in a very early stage, and the contribution to the design process is not well defined.

    As some general rules for now, take this into account before submitting a proposal:

    @@ -61,29 +61,29 @@
    <![CDATA[BEEP 2 - Target and User Persona]]> - https://boosterframework.com/blog/0002-project-target - + https://docs.boosterframework.com/blog/0002-project-target + 2024-01-25T00:00:00.000Z
    STATUS - ACCEPTED
    -

    Introduction

    +

    Introduction

    This document defines the target audience and user persona for the Booster Framework project, aligning it with its current usage and user base.

    While maintaining a focus on guiding the design and development process of the Booster Framework, this definition is not an exclusionary rule but a strategic tool for decision-making. The framework is inclusive and welcomes a diverse range of users, though particular emphasis is placed on specific user groups in design and functionality.

    -

    Target Audience

    +

    Target Audience

    The primary target audience for the Booster Framework is developers in large-scale enterprise environments, familiar with enterprise technologies like Java, .NET, or similar. This includes those working in Fortune 500 companies, especially those transitioning from traditional enterprise technologies to TypeScript in complex projects such as financial transactions.

    Additionally, the Booster Framework is gaining traction among hobbyist developers for personal projects, thanks to its ease of use and efficient learning curve. This secondary audience appreciates the framework for its quick setup, minimal configuration, and straightforward deployment, which are ideal for smaller-scale, personal projects.

    -

    Who's not the target audience

    +

    Who's not the target audience

    • Developers deeply invested in exploring advanced theoretical concepts in programming without practical application.
    • Those who prefer frameworks with a steep learning curve or require extensive configuration.
    -

    User Persona

    -

    Primary Persona: Enterprise Developer

    +

    User Persona

    +

    Primary Persona: Enterprise Developer

    • Name: Aarya
    • Role: Senior Enterprise Software Developer
    -

    Actions, Motivations, and Pains

    +

    Actions, Motivations, and Pains

    • What do I do?
        @@ -110,12 +110,12 @@
    -

    Secondary Persona: Hobbyist Developer

    +

    Secondary Persona: Hobbyist Developer

    • Name: Navin
    • Role: Hobbyist Developer
    -

    Actions, Motivations, and Pains

    +

    Actions, Motivations, and Pains

    • What do I do?
        @@ -142,7 +142,7 @@
    -

    Conclusion

    +

    Conclusion

    This document outlines the dual focus of the Booster Framework's target audience and user personas. Our development and design efforts are directed towards supporting both the enterprise developers transitioning from traditional technologies to TypeScript, and the growing community of hobbyist developers seeking an accessible and easy-to-use framework for personal projects. This approach ensures the Booster Framework remains versatile and relevant across different scales and scopes of software development.

    ]]>
    Nick Tchayka @@ -151,25 +151,25 @@
    <![CDATA[BEEP 3 - Principles of Design]]> - https://boosterframework.com/blog/0003-principles-of-design - + https://docs.boosterframework.com/blog/0003-principles-of-design + 2024-01-25T00:01:00.000Z
    STATUS - ACCEPTED
    -

    Introduction

    -

    This document lays out the design principles guiding the design and implementation processes of the Booster Framework. These principles are crucial in steering both high-level and low-level decision-making within the project. They are pivotal in ensuring that the project remains focused on the correct aspects of design and implementation, always keeping in mind the target audience and user persona, as they are the most important stakeholders of the project.

    -

    Principle of Least Astonishment

    +

    Introduction

    +

    This document lays out the design principles guiding the design and implementation processes of the Booster Framework. These principles are crucial in steering both high-level and low-level decision-making within the project. They are pivotal in ensuring that the project remains focused on the correct aspects of design and implementation, always keeping in mind the target audience and user persona, as they are the most important stakeholders of the project.

    +

    Principle of Least Astonishment

    The Principle of Least Astonishment, also known as the Principle of Least Surprise, is a fundamental guideline in user interface and software design. It stresses the importance of creating systems that behave in ways consistent with user expectations, minimizing surprise and confusion. This principle is crucial in the Booster Framework, ensuring that the framework's components and functionalities align with the conventions familiar to both enterprise and hobbyist developers, thereby enhancing their experience and usability. It is particularly relevant in a context where developers are transitioning from traditional enterprise technologies to modern TypeScript-based environments, as it aids in reducing the learning curve and preventing user astonishment.

    -

    Examples

    +

    Examples

    • Favoring JSON or YAML for configuration over more complex formats, aligning with common industry practices.
    • Integrating with popular version control systems like Git and widely-used platforms such as GitHub or GitLab.
    • Recommending mainstream IDEs like Visual Studio Code, which are familiar to a broad range of developers.
    • Ensuring that the framework's functionalities and syntax are intuitive and align with common programming practices.
    -

    Principle of Developer Happiness

    +

    Principle of Developer Happiness

    The Principle of Developer Happiness is centered around creating an environment and culture that aligns with developers' professional and personal expectations, thereby enhancing satisfaction and retention. This principle is key in the Booster Framework, focusing on an engaging experience and a supportive culture where developers, regardless of their background, feel valued and connected to the project's mission. It also involves using efficient tools and technologies to streamline the development process and saving time, along with continuously assessing developer efficiency and satisfaction.

    -

    Examples

    +

    Examples

    • Comprehensive Documentation: Providing clear and user-friendly documentation with practical examples for both enterprise and hobbyist developers.
    • Active Community Engagement: Encouraging participation and collaboration within the open-source community.
    • @@ -178,9 +178,9 @@
    • Acknowledgement of Contributions: Recognizing and valuing contributions from the community, regardless of their scale.
    • Open Feedback Channels: Maintaining open channels for feedback, suggestions, and issue reporting from users and contributors.
    -

    Principle of Least Effort

    +

    Principle of Least Effort

    The Principle of Least Effort emphasizes the idea that entities will naturally gravitate towards the solution that requires the least amount of work or complexity. In the context of the Booster Framework, this principle is applied to create systems and interfaces that are straightforward, easy to comprehend, and simple to interact with. This reduces the cognitive and operational load on users, particularly those transitioning from different technology backgrounds. For developers, it encourages the creation of code and architectures that are clean, efficient, and easy to understand and modify. By adhering to this principle, the Booster Framework aims to offer user-friendly applications and sustainably maintainable codebases, promoting efficient interactions for all users.

    -

    Examples

    +

    Examples

    • Intuitive Syntax and Features: Designing the framework with a simple, intuitive syntax that reduces cognitive load, particularly for those new to TypeScript.
    • Streamlined Documentation: Providing clear, concise documentation that helps users quickly understand and utilize the framework.
    • @@ -189,7 +189,7 @@
    • Strong Community Support: Building a supportive community for knowledge sharing and collaboration, reducing the effort needed to overcome challenges.
    • Simplified Version Management: Facilitating easy version management and updates for seamless adoption of new features and improvements.
    -

    Conclusion

    +

    Conclusion

    The design principles outlined for the Booster Framework serve as a guiding light for the project's development and implementation processes. The Principle of Least Astonishment ensures system behavior aligns with user expectations, enhancing usability. The Principle of Developer Happiness focuses on creating a fulfilling environment for developers, while the Principle of Least Effort promotes simplicity and efficiency. These principles collectively ensure that the Booster Framework remains attuned to the needs of its diverse user base, from enterprise developers to hobbyist programmers, ensuring a user-centric, efficient, and developer-friendly journey throughout its development.

    ]]>
    Nick Tchayka @@ -198,24 +198,24 @@
    <![CDATA[BEEP 4 - Semantic Versioning]]> - https://boosterframework.com/blog/0004-semantic-versioning - + https://docs.boosterframework.com/blog/0004-semantic-versioning + 2024-01-25T00:03:00.000Z
    STATUS - ACCEPTED
    -

    Introduction

    +

    Introduction

    In the context of the Booster Framework ecosystem, we adopt the Semantic Versioning (SemVer) schema. SemVer is a system of rules and guidelines for assigning and incrementing version numbers within our software development process. By using SemVer, the Booster Framework aims to manage the complexities of dependencies as the ecosystem evolves. The primary goal of implementing Semantic Versioning is to ensure that our version numbers are transparent and informative, effectively communicating the nature of changes in our software to both enterprise and hobbyist developers.

    -

    Impact on Principle of Least Astonishment

    +

    Impact on Principle of Least Astonishment

    Semantic Versioning positively impacts the Principle of Least Astonishment. It is a widely recognized and used schema in numerous open-source projects. Adhering to a clear and consistent version numbering system (Major.Minor.Patch) reduces confusion and surprise for developers and users alike. This predictability in versioning enhances the user experience and facilitates the management of software dependencies.

    -

    Impact on Principle of Developer Happiness

    +

    Impact on Principle of Developer Happiness

    Semantic Versioning aligns with the Principle of Developer Happiness by offering a systematic and standardized approach to versioning. This method simplifies the process of releasing and updating software packages, allowing developers to confidently implement changes. Knowing that version numbers accurately reflect the impact of changes reduces the stress related to dependency management and enables developers to concentrate on innovation and software improvement.

    -

    Impact on Principle of Least Effort

    +

    Impact on Principle of Least Effort

    Semantic Versioning supports the Principle of Least Effort by making the management of software dependencies more straightforward. By adhering to SemVer, developers can introduce backward-compatible changes without needing new major releases, thereby minimizing the effort needed for maintaining and updating software. Additionally, the clear documentation of public APIs and the use of version numbers to indicate compatibility simplify the integration of dependencies, reducing the effort needed for seamless software interactions.

    -

    Usage of SemVer in Early Development Phases

    +

    Usage of SemVer in Early Development Phases

    During the initial development phases of Booster Framework projects, the major version will remain at 0. This indicates that breaking changes are likely to be frequent as the project evolves. Once the project achieves a stable state, the major version will be incremented to 1, signifying a reduction in the frequency of breaking changes.

    -

    Standardizing the Use of SemVer

    +

    Standardizing the Use of SemVer

    We advise using the Semantic Versioning schema as a standard across all Booster Framework projects. This uniformity will ensure coherence within the ecosystem. The Booster CLI tool should facilitate version management by inspecting exported functions and types, suggesting the next version based on the changes made.

    -

    Considerations

    +

    Considerations

    It's important to note that changes in the implementation of a function, even without altering the API, can produce different results and are considered breaking changes. These should be reflected in the version number accordingly. For more information and examples, please refer to this GitHub thread.

    ]]>
    Nick Tchayka @@ -224,24 +224,24 @@
    <![CDATA[BEEP 5 - Agent-Based Codebase Structure]]> - https://boosterframework.com/blog/0005-agent-codebase - + https://docs.boosterframework.com/blog/0005-agent-codebase + 2024-01-25T00:04:00.000Z
    STATUS - DRAFT
    -

    Introduction

    +

    Introduction

    This document proposes a significant reorganization of the Booster Framework's folder structure to address current challenges in codebase management and to align with the principles of modularity and scalability. The current structure, src/{commands,events,entities,read-models}/*.ts, while functional, has shown limitations in managing complexity as the framework and the team grow.

    This proposal introduces a new structure based on the concept of "Agents" in event sourcing/event-driven systems, aiming to bring conceptual clarity and improved organization to the codebase. It can be thought as the microservices approach, but more tailored to the event-driven nature of Booster Framework.

    -

    Current Challenges

    +

    Current Challenges

    The existing folder structure in the Booster Framework presents several challenges:

    • Lack of Modularity: The mixed nature of components (commands, events, entities, read-models) in a flat structure leads to difficulties in isolating specific functionalities or use cases.
    • Scalability Concerns: As the team and the codebase grow, the lack of hierarchical organization makes it increasingly challenging to manage and navigate the codebase.
    • Interdependency Issues: The current structure does not clearly delineate dependencies and relationships between different components, leading to potential conflicts and complexities.
    -

    Proposed Folder Structure

    +

    Proposed Folder Structure

    The new folder structure is organized around the concept of "Agents", where each use case is encapsulated within its own subfolder. This approach mirrors the modular and event-driven nature of the Booster Framework.

    -

    Structure Overview

    +

    Structure Overview

    • src/
        @@ -276,25 +276,25 @@
    -

    Guidelines for Naming Subfolders

    +

    Guidelines for Naming Subfolders

    • Reflective of Use Case: Each subfolder (agent) should be named in a way that clearly reflects its specific use case or domain.
    • Consistent Naming Convention: A uniform naming convention should be adopted across all agents to maintain consistency and readability.
    -

    Benefits of the Reorganization

    +

    Benefits of the Reorganization

    • Improved Modularity: Each agent acts as a self-contained module, enhancing the clarity and separation of different aspects of the codebase.
    • Enhanced Scalability: This structure supports the growth of the team and the codebase, allowing for easier navigation and management.
    • Facilitated Collaboration: Teams can focus on specific agents without interfering with others, simplifying collaboration and reducing the risk of conflicts.
    • Repository Splitting: The modular nature of this structure allows for splitting the codebase into separate repositories if necessary for better scalability and management.
    -

    Addressing Potential Challenges

    -

    Interaction and Communication Between Agents

    +

    Addressing Potential Challenges

    +

    Interaction and Communication Between Agents

    • Well-Defined Interfaces: Clear interfaces and communication protocols between agents must be established to ensure smooth interactions.
    • Shared Resources Management: The shared/ folder will house common entities and utilities, accessible to all agents while maintaining their independence.
    -

    Technical and Implementation Considerations

    +

    Technical and Implementation Considerations

    \ No newline at end of file diff --git a/going-deeper/custom-templates/index.html b/going-deeper/custom-templates/index.html index aa9b19710..0a2ee8cc6 100644 --- a/going-deeper/custom-templates/index.html +++ b/going-deeper/custom-templates/index.html @@ -3,16 +3,16 @@ -Customizing CLI resource templates | Booster Framework +Customizing CLI resource templates | Booster Framework - - - + + + +
    I have another question!

    You can ask questions on our Discord channel or create discussion on Github.

    \ No newline at end of file diff --git a/going-deeper/data-migrations/index.html b/going-deeper/data-migrations/index.html index be0cf9d7f..999fcaa60 100644 --- a/going-deeper/data-migrations/index.html +++ b/going-deeper/data-migrations/index.html @@ -3,16 +3,16 @@ -Migrations | Booster Framework +Migrations | Booster Framework - - - + + +

    Migrations

    @@ -62,6 +62,6 @@

    const AzureWebhook = (params: WebhookParams): InfrastructureRocket => ({
    mountStack: Synth.mountStack.bind(Synth, params),
    mountCode: Functions.mountCode.bind(Synth, params),
    getFunctionAppName: Functions.getFunctionAppName.bind(Synth, params),
    })

    This method will return an Array of functions definitions, the function name, and the host.json file. Example:

    export interface FunctionAppFunctionsDefinition<T extends Binding = Binding> {
    functionAppName: string
    functionsDefinitions: Array<FunctionDefinition<T>>
    hostJsonPath?: string
    }
    -

    Booster 2.3.0 allows you to set the app service plan used to deploy the main function app. Setting the BOOSTER_AZURE_SERVICE_PLAN_BASIC environment variable to true will force the use of a basic service plan instead of the default consumption plan.

    +

    Booster 2.3.0 allows you to set the app service plan used to deploy the main function app. Setting the BOOSTER_AZURE_SERVICE_PLAN_BASIC environment variable to true will force the use of a basic service plan instead of the default consumption plan.

    \ No newline at end of file diff --git a/going-deeper/environment-configuration/index.html b/going-deeper/environment-configuration/index.html index 1d9f309f0..281912a1a 100644 --- a/going-deeper/environment-configuration/index.html +++ b/going-deeper/environment-configuration/index.html @@ -3,16 +3,16 @@ -Environments | Booster Framework +Environments | Booster Framework - - - + + +

    Environments

    @@ -24,6 +24,6 @@
    boost deploy -e prod

    This way, you can have different configurations depending on your needs.

    Booster environments are extremely flexible. As shown in the first example, your 'fruit-store' app can have three team-wide environments: 'dev', 'stage', and 'prod', each of them with different app names or providers, that are deployed by your CI/CD processes. Developers, like "John" in the second example, can create their own private environments in separate config files to test their changes in realistic environments before committing them. Likewise, CI/CD processes could generate separate production-like environments to test different branches to perform QA in separate environments without interferences from other features under test.

    -

    The only thing you need to do to deploy a whole new completely-independent copy of your application is to use a different name. Also, Booster uses the credentials available in the machine (~/.aws/credentials in AWS) that performs the deployment process, so developers can even work on separate accounts than production or staging environments.

    +

    The only thing you need to do to deploy a whole new completely-independent copy of your application is to use a different name. Also, Booster uses the credentials available in the machine (~/.aws/credentials in AWS) that performs the deployment process, so developers can even work on separate accounts than production or staging environments.

    \ No newline at end of file diff --git a/going-deeper/framework-packages/index.html b/going-deeper/framework-packages/index.html index 387c604cf..9fe685cd3 100644 --- a/going-deeper/framework-packages/index.html +++ b/going-deeper/framework-packages/index.html @@ -3,16 +3,16 @@ -Framework packages | Booster Framework +Framework packages | Booster Framework - - - + + +

    Framework packages

    @@ -21,6 +21,6 @@

    Framework Cor

    The framework-core package includes the most important components of the framework abstraction. It can be seen as skeleton or the main architecture of the framework.

    The package defines the specification of how should a Booster application work without taking into account the specific providers that could be used. Every Booster provider package is based on the components that the framework core needs in order to work on the platform.

    Framework Types

    -

    The framework-types packages includes the types that define the domain of the Booster framework. It defines domain concepts like an Event, a Command or a Role.

    +

    The framework-types packages includes the types that define the domain of the Booster framework. It defines domain concepts like an Event, a Command or a Role.

    \ No newline at end of file diff --git a/going-deeper/health/sensor-health/index.html b/going-deeper/health/sensor-health/index.html index 74b416c3d..e42c5699e 100644 --- a/going-deeper/health/sensor-health/index.html +++ b/going-deeper/health/sensor-health/index.html @@ -3,16 +3,16 @@ -sensor-health | Booster Framework +sensor-health | Booster Framework - - - + + + +
    └── events
    \ No newline at end of file diff --git a/going-deeper/infrastructure-providers/index.html b/going-deeper/infrastructure-providers/index.html index 3230382ab..704a8aab7 100644 --- a/going-deeper/infrastructure-providers/index.html +++ b/going-deeper/infrastructure-providers/index.html @@ -3,16 +3,16 @@ -Configuring Infrastructure Providers | Booster Framework +Configuring Infrastructure Providers | Booster Framework - - - + + + +

    This action will clear the local data and allow you to proceed with your new changes effectively.

    \ No newline at end of file diff --git a/going-deeper/instrumentation/index.html b/going-deeper/instrumentation/index.html index 4131d18c5..87be6165f 100644 --- a/going-deeper/instrumentation/index.html +++ b/going-deeper/instrumentation/index.html @@ -3,16 +3,16 @@ -Booster instrumentation | Booster Framework +Booster instrumentation | Booster Framework - - - + + +

    Booster instrumentation

    @@ -40,6 +40,6 @@

    import { Trace } from '@boostercloud/framework-core'
    import { BoosterConfig, Logger } from '@boostercloud/framework-types'

    export class MyCustomClass {
    @Trace('OTHER')
    public async myCustomMethod(config: BoosterConfig, logger: Logger): Promise<void> {
    logger.debug('This is my custom method')
    // Do some custom logic here...
    }
    }

    In the example above, we added the @Trace('OTHER') decorator to the myCustomMethod method. This will cause the method to emit trace events when it's invoked, allowing you to trace the flow of your application and detect performance bottlenecks or errors.

    -

    Note that when you add the Trace Decorator to your own methods, you'll need to configure your Booster instance to use a tracer that implements the necessary methods to handle these events.

    +

    Note that when you add the Trace Decorator to your own methods, you'll need to configure your Booster instance to use a tracer that implements the necessary methods to handle these events.

    \ No newline at end of file diff --git a/going-deeper/register/index.html b/going-deeper/register/index.html index 02ceeb963..cb3f410c5 100644 --- a/going-deeper/register/index.html +++ b/going-deeper/register/index.html @@ -3,16 +3,16 @@ -Advanced uses of the Register object | Booster Framework +Advanced uses of the Register object | Booster Framework - - - + + +

    Advanced uses of the Register object

    @@ -48,6 +48,6 @@

    A

    The rawContext property exposes the full raw request context as it comes in the original request, so it will depend on the underlying provider used. For instance, in AWS, it will be a lambda context object, while in Azure it will be an Azure Functions context object.

    Alter the HTTP response headers

    Finally, you can use the responseHeaders property to alter the HTTP response headers that will be sent back to the client. This property is a plain Typescript object which is initialized with the default headers. You can add, remove or modify any of the headers by using the standard object methods:

    -
    public async handle(register: Register): Promise<void> {
    register.responseHeaders['X-My-Header'] = 'My custom header'
    register.responseHeaders['X-My-Other-Header'] = 'My other custom header'
    delete register.responseHeaders['X-My-Other-Header']
    }
    +
    public async handle(register: Register): Promise<void> {
    register.responseHeaders['X-My-Header'] = 'My custom header'
    register.responseHeaders['X-My-Other-Header'] = 'My other custom header'
    delete register.responseHeaders['X-My-Other-Header']
    }
    \ No newline at end of file diff --git a/going-deeper/rockets/index.html b/going-deeper/rockets/index.html index 09c6d199a..9bd843d93 100644 --- a/going-deeper/rockets/index.html +++ b/going-deeper/rockets/index.html @@ -3,16 +3,16 @@ -Extending Booster with Rockets! | Booster Framework +Extending Booster with Rockets! | Booster Framework - - - + + +
    +
    \ No newline at end of file diff --git a/going-deeper/rockets/rocket-backup-booster/index.html b/going-deeper/rockets/rocket-backup-booster/index.html index ea183f8d0..b5f079ed1 100644 --- a/going-deeper/rockets/rocket-backup-booster/index.html +++ b/going-deeper/rockets/rocket-backup-booster/index.html @@ -3,16 +3,16 @@ -Backup Booster Rocket | Booster Framework +Backup Booster Rocket | Booster Framework - - - + + +
    +
    src/config/config.ts
    import { Booster } from '@boostercloud/framework-core'
    import { BoosterConfig } from '@boostercloud/framework-types'
    import * as AWS from '@boostercloud/framework-provider-aws'

    Booster.configure('development', (config: BoosterConfig): void => {
    config.appName = 'my-store'
    config.provider = Provider([{
    packageName: '@boostercloud/rocket-backup-aws-infrastructure',
    parameters: {
    backupType: 'ON_DEMAND', // or 'POINT_IN_TIME'
    // onDemandBackupRules is optional and uses cron notation. Cron params are all optional too.
    onDemandBackupRules: {
    minute: '30',
    hour: '3',
    day: '15',
    month: '5',
    weekDay: '4', // Weekday is also supported, but can't be set along with 'day' parameter
    year: '2077',
    }
    }
    }])
    })
    \ No newline at end of file diff --git a/going-deeper/rockets/rocket-file-uploads/index.html b/going-deeper/rockets/rocket-file-uploads/index.html index 1358f7a3a..417a3fe50 100644 --- a/going-deeper/rockets/rocket-file-uploads/index.html +++ b/going-deeper/rockets/rocket-file-uploads/index.html @@ -3,16 +3,16 @@ -File Uploads Rocket | Booster Framework +File Uploads Rocket | Booster Framework - - - + + + + \ No newline at end of file diff --git a/going-deeper/rockets/rocket-static-sites/index.html b/going-deeper/rockets/rocket-static-sites/index.html index a60622b3e..da195012b 100644 --- a/going-deeper/rockets/rocket-static-sites/index.html +++ b/going-deeper/rockets/rocket-static-sites/index.html @@ -3,16 +3,16 @@ -Static Sites Rocket | Booster Framework +Static Sites Rocket | Booster Framework - - - + + +
    +
    import { Booster } from '@boostercloud/framework-core'
    import { BoosterConfig } from '@boostercloud/framework-types'

    Booster.configure('development', (config: BoosterConfig): void => {
    config.appName = 'my-store'
    config.rockets = [
    {
    packageName: '@boostercloud/rocket-static-sites-aws-infrastructure',
    parameters: {
    bucketName: 'test-bucket-name', // Required
    rootPath: './frontend/dist', // Defaults to ./public
    indexFile: 'main.html', // File to render when users access the CLoudFormation URL. Defaults to index.html
    errorFile: 'error.html', // File to render when there's an error. Defaults to 404.html
    }
    },
    ]
    })
    \ No newline at end of file diff --git a/going-deeper/rockets/rocket-webhook/index.html b/going-deeper/rockets/rocket-webhook/index.html index 6693a2442..b853dbcb3 100644 --- a/going-deeper/rockets/rocket-webhook/index.html +++ b/going-deeper/rockets/rocket-webhook/index.html @@ -3,16 +3,16 @@ -Webhook Rocket | Booster Framework +Webhook Rocket | Booster Framework - - - + + +

    Webhook Rocket

    @@ -41,6 +41,6 @@

    Return typeDemo

    curl --request POST 'http://localhost:3000/webhook/command?param1=testvalue'

    The webhookEventInterface object will be similar to this one:

    -
    {
    origin: 'test',
    method: 'POST',
    url: '/test?param1=testvalue',
    originalUrl: '/webhook/test?param1=testvalue',
    headers: {
    accept: '*/*',
    'cache-control': 'no-cache',
    host: 'localhost:3000',
    'accept-encoding': 'gzip, deflate, br',
    connection: 'keep-alive',
    'content-length': '0'
    },
    query: { param1: 'testvalue' },
    params: {},
    rawBody: undefined,
    body: {}
    }
    +
    {
    origin: 'test',
    method: 'POST',
    url: '/test?param1=testvalue',
    originalUrl: '/webhook/test?param1=testvalue',
    headers: {
    accept: '*/*',
    'cache-control': 'no-cache',
    host: 'localhost:3000',
    'accept-encoding': 'gzip, deflate, br',
    connection: 'keep-alive',
    'content-length': '0'
    },
    query: { param1: 'testvalue' },
    params: {},
    rawBody: undefined,
    body: {}
    }
    \ No newline at end of file diff --git a/going-deeper/sensor/index.html b/going-deeper/sensor/index.html index 29ab07db0..4d5cf475e 100644 --- a/going-deeper/sensor/index.html +++ b/going-deeper/sensor/index.html @@ -3,19 +3,19 @@ -Sensor | Booster Framework +Sensor | Booster Framework - - - + + + +
    \ No newline at end of file diff --git a/going-deeper/testing/index.html b/going-deeper/testing/index.html index 8ce05e5dd..41b807d20 100644 --- a/going-deeper/testing/index.html +++ b/going-deeper/testing/index.html @@ -3,16 +3,16 @@ -Testing | Booster Framework +Testing | Booster Framework - - - + + + + \ No newline at end of file diff --git a/going-deeper/touch-entities/index.html b/going-deeper/touch-entities/index.html index a27fffeaf..5431946f3 100644 --- a/going-deeper/touch-entities/index.html +++ b/going-deeper/touch-entities/index.html @@ -3,16 +3,16 @@ -TouchEntities | Booster Framework +TouchEntities | Booster Framework - - - + + +

    TouchEntities

    @@ -23,6 +23,6 @@ For example, this command will touch all the entities of the class Cart.:

    import { Booster, BoosterTouchEntityHandler, Command } from '@boostercloud/framework-core'
    import { Register } from '@boostercloud/framework-types'
    import { Cart } from '../entities/cart'

    @Command({
    authorize: 'all',
    })
    export class TouchCommand {
    public constructor() {}

    public static async handle(_command: TouchCommand, _register: Register): Promise<void> {
    const entitiesIdsResult = await Booster.entitiesIDs('Cart', 500, undefined)
    const paginatedEntityIdResults = entitiesIdsResult.items
    const carts = await Promise.all(
    paginatedEntityIdResults.map(async (entity) => await Booster.entity(Cart, entity.entityID))
    )
    if (!carts || carts.length === 0) {
    return
    }
    await Promise.all(
    carts.map(async (cart) => {
    const validCart = cart!
    await BoosterTouchEntityHandler.touchEntity('Cart', validCart.id)
    console.log('Touched', validCart)
    return validCart.id
    })
    )
    }
    }

    Please note that touching entities is an advanced feature that should be used with caution and only when necessary. -It may affect your application performance and consistency if not used properly.

    +It may affect your application performance and consistency if not used properly.

    \ No newline at end of file diff --git a/graphql/index.html b/graphql/index.html index 3f6870b6a..77a6f6612 100644 --- a/graphql/index.html +++ b/graphql/index.html @@ -3,16 +3,16 @@ -GraphQL API | Booster Framework +GraphQL API | Booster Framework - - - + + +
    +
    note

    The WebSocket communication in Booster only supports this subprotocol, whose identifier is graphql-ws. For this reason, when you connect to the WebSocket provisioned by Booster, you must specify the graphql-ws subprotocol. If not, the connection won't succeed.

    \ No newline at end of file diff --git a/index.html b/index.html index 162c02fda..5bce2e315 100644 --- a/index.html +++ b/index.html @@ -3,19 +3,19 @@ -Ask about Booster Framework | Booster Framework +Ask about Booster Framework | Booster Framework - - - + + +

    Ask about Booster Framework

    -
    PrivateGPT · Free beta version
    +
    PrivateGPT · Free beta version
    \ No newline at end of file diff --git a/introduction/index.html b/introduction/index.html index 58cb64051..c76b91651 100644 --- a/introduction/index.html +++ b/introduction/index.html @@ -3,16 +3,16 @@ -Introduction | Booster Framework +Introduction | Booster Framework - - - + + +

    Introduction

    @@ -64,6 +64,6 @@

    Why use Boos
  • Event-sourcing by default: Booster keeps all incremental data changes as events, indefinitely. This means that any previous state of the system can be recreated and replayed at any moment, enabling a whole world of possibilities for troubleshooting and auditing, syncing environments or performing tests and simulations.
  • Booster makes it easy to build enterprise-grade applications: Implementing an event-sourcing system from scratch is a challenging exercise that usually requires highly specialized experts. There are some technical challenges like eventual consistency, message ordering, and snapshot building. Booster takes care of all of that and more for you, lowering the curve for people that are starting and making expert lives easier.
  • Choose your application cloud and avoid vendor lock-in: Booster provides a highly decoupled architecture that enables the possibility of integrating with ease new providers with different specifications, including a custom Multi-cloud provider, without affecting the framework specification.
  • -

    + \ No newline at end of file diff --git a/opensearch.xml b/opensearch.xml index 4142add22..6546b1c30 100644 --- a/opensearch.xml +++ b/opensearch.xml @@ -4,8 +4,8 @@ Booster Framework Search Booster Framework UTF-8 - https://boosterframework.com/img/favicon.png - - - https://boosterframework.com/ + https://docs.boosterframework.com/img/favicon.png + + + https://docs.boosterframework.com/ \ No newline at end of file diff --git a/search/index.html b/search/index.html index 9e7db5164..fbe403f67 100644 --- a/search/index.html +++ b/search/index.html @@ -3,16 +3,16 @@ -Search the documentation | Booster Framework +Search the documentation | Booster Framework - - - + + +

    Search the documentation

    diff --git a/security/authentication/index.html b/security/authentication/index.html index a1cb25505..e2682caa3 100644 --- a/security/authentication/index.html +++ b/security/authentication/index.html @@ -3,16 +3,16 @@ -Authentication | Booster Framework +Authentication | Booster Framework - - - + + +

    Authentication

    @@ -50,6 +50,6 @@

    Advanced authentication

    If you need to do more advanced checks, you can implement the whole verification algorithm yourself. For example, if you're using non-standard or legacy tokens. Booster exposes for convenience many of the utility functions that it uses in the default TokenVerifier implementations:

    FunctionDescription
    getJwksClientInitializes a jwksRSA client that can be used to get the public key of a JWKS URI using the getKeyWithClient function.
    getKeyWithClientInitializes a function that can be used to get the public key from a JWKS URI with the signature required by the verifyJWT function. You can create a client using the getJwksClient function.
    verifyJWTVerifies a JWT token using a key or key resolver function and returns a Booster UserEnvelope.
    -
    /**
    * Initializes a jwksRSA client that can be used to get the public key of a JWKS URI using the
    * `getKeyWithClient` function.
    */
    export function getJwksClient(jwksUri: string) {
    ...
    }

    /**
    * Initializes a function that can be used to get the public key from a JWKS URI with the signature
    * required by the `verifyJWT` function. You can create a client using the `getJwksClient` function.
    */
    export function getKeyWithClient(
    client: jwksRSA.JwksClient,
    header: jwt.JwtHeader,
    callback: jwt.SigningKeyCallback
    ): void {
    ...
    }

    /**
    * Verifies a JWT token using a key or key resolver function and returns a Booster UserEnvelope.
    */
    export async function verifyJWT(
    token: string,
    issuer: string,
    key: jwt.Secret | jwt.GetPublicKeyOrSecret,
    rolesClaim?: string
    ): Promise<UserEnvelope> {
    ...
    }
    +
    /**
    * Initializes a jwksRSA client that can be used to get the public key of a JWKS URI using the
    * `getKeyWithClient` function.
    */
    export function getJwksClient(jwksUri: string) {
    ...
    }

    /**
    * Initializes a function that can be used to get the public key from a JWKS URI with the signature
    * required by the `verifyJWT` function. You can create a client using the `getJwksClient` function.
    */
    export function getKeyWithClient(
    client: jwksRSA.JwksClient,
    header: jwt.JwtHeader,
    callback: jwt.SigningKeyCallback
    ): void {
    ...
    }

    /**
    * Verifies a JWT token using a key or key resolver function and returns a Booster UserEnvelope.
    */
    export async function verifyJWT(
    token: string,
    issuer: string,
    key: jwt.Secret | jwt.GetPublicKeyOrSecret,
    rolesClaim?: string
    ): Promise<UserEnvelope> {
    ...
    }
    \ No newline at end of file diff --git a/security/authorization/index.html b/security/authorization/index.html index 152922558..2c15a836e 100644 --- a/security/authorization/index.html +++ b/security/authorization/index.html @@ -3,16 +3,16 @@ -Authorization | Booster Framework +Authorization | Booster Framework - - - + + +

    Authorization

    @@ -69,6 +69,6 @@

    Eve

    You can restrict the access to the Event Stream of an Entity by providing an authorizeReadEvents function in the @Entity decorator. This function is called every time an event stream is requested. The function must match the EventStreamAuthorizer type receives the current user and the event search request as parameters. The function must return a Promise<void>. If the promise is rejected, the request will be denied. If the promise is resolved successfully, the request will be allowed.

    export type EventStreamAuthorizer = (
    currentUser?: UserEnvelope,
    eventSearchRequest?: EventSearchRequest
    ) => Promise<void>

    For instance, you can restrict access to entities that the current user own.

    -
    const CustomEventAuthorizer: EventStreamAuthorizer = async (currentUser, eventSearchRequest) => {
    const { entityID } = eventSearchRequest.parameters
    if (!entityID) {
    throw new Error(`${currentUser.username} cannot list carts`)
    }
    const cart = Booster.entity(Cart, entityID)
    if (cart.ownerUserName !== currentUser.userName) {
    throw new Error(`${currentUser.username} cannot see events in cart ${entityID}`)
    }
    }


    @Entity({
    authorizeReadEvents: CustomEventAuthorizer
    })
    export class Cart {
    public constructor(
    readonly id: UUID,
    readonly ownerUserName: string,
    readonly cartItems: Array<CartItem>,
    public shippingAddress?: Address,
    public checks = 0
    ) {}
    ...
    }

    +
    const CustomEventAuthorizer: EventStreamAuthorizer = async (currentUser, eventSearchRequest) => {
    const { entityID } = eventSearchRequest.parameters
    if (!entityID) {
    throw new Error(`${currentUser.username} cannot list carts`)
    }
    const cart = Booster.entity(Cart, entityID)
    if (cart.ownerUserName !== currentUser.userName) {
    throw new Error(`${currentUser.username} cannot see events in cart ${entityID}`)
    }
    }


    @Entity({
    authorizeReadEvents: CustomEventAuthorizer
    })
    export class Cart {
    public constructor(
    readonly id: UUID,
    readonly ownerUserName: string,
    readonly cartItems: Array<CartItem>,
    public shippingAddress?: Address,
    public checks = 0
    ) {}
    ...
    }
    \ No newline at end of file diff --git a/security/security/index.html b/security/security/index.html index e4ece6766..b8a7f1ed9 100644 --- a/security/security/index.html +++ b/security/security/index.html @@ -3,16 +3,16 @@ -Security | Booster Framework +Security | Booster Framework - - - + + + +
    \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml index 590beaf4a..fe17c9ad4 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1 +1 @@ -https://boosterframework.com/blogweekly0.5https://boosterframework.com/blog/0000-indexweekly0.5https://boosterframework.com/blog/0001-purpose-and-guidelinesweekly0.5https://boosterframework.com/blog/0002-project-targetweekly0.5https://boosterframework.com/blog/0003-principles-of-designweekly0.5https://boosterframework.com/blog/0004-semantic-versioningweekly0.5https://boosterframework.com/blog/0005-agent-codebaseweekly0.5https://boosterframework.com/blog/0006-remote-importsweekly0.5https://boosterframework.com/blog/archiveweekly0.5https://boosterframework.com/Homeweekly0.5https://boosterframework.com/searchweekly0.5https://boosterframework.com/architecture/commandweekly0.5https://boosterframework.com/architecture/entityweekly0.5https://boosterframework.com/architecture/eventweekly0.5https://boosterframework.com/architecture/event-drivenweekly0.5https://boosterframework.com/architecture/event-handlerweekly0.5https://boosterframework.com/architecture/notificationsweekly0.5https://boosterframework.com/architecture/queriesweekly0.5https://boosterframework.com/architecture/read-modelweekly0.5https://boosterframework.com/booster-cliweekly0.5https://boosterframework.com/category/featuresweekly0.5https://boosterframework.com/category/getting-startedweekly0.5https://boosterframework.com/category/going-deeper-with-boosterweekly0.5https://boosterframework.com/contributingweekly0.5https://boosterframework.com/features/error-handlingweekly0.5https://boosterframework.com/features/event-streamweekly0.5https://boosterframework.com/features/loggingweekly0.5https://boosterframework.com/features/schedule-actionsweekly0.5https://boosterframework.com/frequently-asked-questionsweekly0.5https://boosterframework.com/getting-started/codingweekly0.5https://boosterframework.com/getting-started/installationweekly0.5https://boosterframework.com/going-deeper/azure-scaleweekly0.5https://boosterframework.com/going-deeper/custom-providersweekly0.5https://boosterframework.com/going-deeper/custom-templatesweekly0.5https://boosterframework.com/going-deeper/data-migrationsweekly0.5https://boosterframework.com/going-deeper/environment-configurationweekly0.5https://boosterframework.com/going-deeper/framework-packagesweekly0.5https://boosterframework.com/going-deeper/health/sensor-healthweekly0.5https://boosterframework.com/going-deeper/infrastructure-providersweekly0.5https://boosterframework.com/going-deeper/instrumentationweekly0.5https://boosterframework.com/going-deeper/registerweekly0.5https://boosterframework.com/going-deeper/rocketsweekly0.5https://boosterframework.com/going-deeper/rockets/rocket-backup-boosterweekly0.5https://boosterframework.com/going-deeper/rockets/rocket-file-uploadsweekly0.5https://boosterframework.com/going-deeper/rockets/rocket-static-sitesweekly0.5https://boosterframework.com/going-deeper/rockets/rocket-webhookweekly0.5https://boosterframework.com/going-deeper/sensorweekly0.5https://boosterframework.com/going-deeper/testingweekly0.5https://boosterframework.com/going-deeper/touch-entitiesweekly0.5https://boosterframework.com/graphqlweekly0.5https://boosterframework.com/introductionweekly0.5https://boosterframework.com/security/authenticationweekly0.5https://boosterframework.com/security/authorizationweekly0.5https://boosterframework.com/security/securityweekly0.5https://boosterframework.com/weekly0.5 \ No newline at end of file +https://docs.boosterframework.com/blogweekly0.5https://docs.boosterframework.com/blog/0000-indexweekly0.5https://docs.boosterframework.com/blog/0001-purpose-and-guidelinesweekly0.5https://docs.boosterframework.com/blog/0002-project-targetweekly0.5https://docs.boosterframework.com/blog/0003-principles-of-designweekly0.5https://docs.boosterframework.com/blog/0004-semantic-versioningweekly0.5https://docs.boosterframework.com/blog/0005-agent-codebaseweekly0.5https://docs.boosterframework.com/blog/0006-remote-importsweekly0.5https://docs.boosterframework.com/blog/archiveweekly0.5https://docs.boosterframework.com/Homeweekly0.5https://docs.boosterframework.com/searchweekly0.5https://docs.boosterframework.com/architecture/commandweekly0.5https://docs.boosterframework.com/architecture/entityweekly0.5https://docs.boosterframework.com/architecture/eventweekly0.5https://docs.boosterframework.com/architecture/event-drivenweekly0.5https://docs.boosterframework.com/architecture/event-handlerweekly0.5https://docs.boosterframework.com/architecture/notificationsweekly0.5https://docs.boosterframework.com/architecture/queriesweekly0.5https://docs.boosterframework.com/architecture/read-modelweekly0.5https://docs.boosterframework.com/booster-cliweekly0.5https://docs.boosterframework.com/category/featuresweekly0.5https://docs.boosterframework.com/category/getting-startedweekly0.5https://docs.boosterframework.com/category/going-deeper-with-boosterweekly0.5https://docs.boosterframework.com/contributingweekly0.5https://docs.boosterframework.com/features/error-handlingweekly0.5https://docs.boosterframework.com/features/event-streamweekly0.5https://docs.boosterframework.com/features/loggingweekly0.5https://docs.boosterframework.com/features/schedule-actionsweekly0.5https://docs.boosterframework.com/frequently-asked-questionsweekly0.5https://docs.boosterframework.com/getting-started/codingweekly0.5https://docs.boosterframework.com/getting-started/installationweekly0.5https://docs.boosterframework.com/going-deeper/azure-scaleweekly0.5https://docs.boosterframework.com/going-deeper/custom-providersweekly0.5https://docs.boosterframework.com/going-deeper/custom-templatesweekly0.5https://docs.boosterframework.com/going-deeper/data-migrationsweekly0.5https://docs.boosterframework.com/going-deeper/environment-configurationweekly0.5https://docs.boosterframework.com/going-deeper/framework-packagesweekly0.5https://docs.boosterframework.com/going-deeper/health/sensor-healthweekly0.5https://docs.boosterframework.com/going-deeper/infrastructure-providersweekly0.5https://docs.boosterframework.com/going-deeper/instrumentationweekly0.5https://docs.boosterframework.com/going-deeper/registerweekly0.5https://docs.boosterframework.com/going-deeper/rocketsweekly0.5https://docs.boosterframework.com/going-deeper/rockets/rocket-backup-boosterweekly0.5https://docs.boosterframework.com/going-deeper/rockets/rocket-file-uploadsweekly0.5https://docs.boosterframework.com/going-deeper/rockets/rocket-static-sitesweekly0.5https://docs.boosterframework.com/going-deeper/rockets/rocket-webhookweekly0.5https://docs.boosterframework.com/going-deeper/sensorweekly0.5https://docs.boosterframework.com/going-deeper/testingweekly0.5https://docs.boosterframework.com/going-deeper/touch-entitiesweekly0.5https://docs.boosterframework.com/graphqlweekly0.5https://docs.boosterframework.com/introductionweekly0.5https://docs.boosterframework.com/security/authenticationweekly0.5https://docs.boosterframework.com/security/authorizationweekly0.5https://docs.boosterframework.com/security/securityweekly0.5https://docs.boosterframework.com/weekly0.5 \ No newline at end of file