diff --git a/404.html b/404.html index edce2670b..ef5a7fd58 100644 --- a/404.html +++ b/404.html @@ -11,7 +11,7 @@ - + diff --git a/Home/index.html b/Home/index.html index 88cc7a4e3..e64b01383 100644 --- a/Home/index.html +++ b/Home/index.html @@ -11,7 +11,7 @@ - + diff --git a/architecture/command/index.html b/architecture/command/index.html index 64eb3df13..608a904f0 100644 --- a/architecture/command/index.html +++ b/architecture/command/index.html @@ -11,7 +11,7 @@ - + @@ -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 7e0eea274..7a672ca0e 100644 --- a/architecture/entity/index.html +++ b/architecture/entity/index.html @@ -11,7 +11,7 @@ - + @@ -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 18de7478d..d244ba4a7 100644 --- a/architecture/event-driven/index.html +++ b/architecture/event-driven/index.html @@ -11,7 +11,7 @@ - + @@ -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.

    -
    +
    \ No newline at end of file diff --git a/architecture/event-handler/index.html b/architecture/event-handler/index.html index 97aac35aa..be86d08d4 100644 --- a/architecture/event-handler/index.html +++ b/architecture/event-handler/index.html @@ -11,7 +11,7 @@ - + @@ -40,6 +40,6 @@

    Creating a global event handler

    Booster includes a Global event handler. This feature allows you to react to any event that occurs within the system. By annotating a class with the @GlobalEventHandler decorator, the handle method within that class will be automatically called for any event that is generated

    -
    @GlobalEventHandler
    export class GlobalHandler {
    public static async handle(event: EventInterface | NotificationInterface, register: Register): Promise<void> {
    if (event instanceof LogEventReceived) {
    register.events(new LogEventReceivedTest(event.entityID(), event.value))
    }
    }
    +
    @GlobalEventHandler
    export class GlobalHandler {
    public static async handle(event: EventInterface | NotificationInterface, register: Register): Promise<void> {
    if (event instanceof LogEventReceived) {
    register.events(new LogEventReceivedTest(event.entityID(), event.value))
    }
    }
    \ No newline at end of file diff --git a/architecture/event/index.html b/architecture/event/index.html index 89c823505..b97333223 100644 --- a/architecture/event/index.html +++ b/architecture/event/index.html @@ -11,7 +11,7 @@ - + @@ -46,6 +46,6 @@

    Eve
  • StockMoved
  • As with other Booster files, events have their own directory:

    -
    <project-root>
    ├── src
    │ ├── commands
    │ ├── common
    │ ├── config
    │ ├── entities
    │ ├── events <------ put them here
    │ ├── index.ts
    │ └── read-models
    +
    <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 0c7e5de73..f93910410 100644 --- a/architecture/notifications/index.html +++ b/architecture/notifications/index.html @@ -11,7 +11,7 @@ - + @@ -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 649741567..25bcdc256 100644 --- a/architecture/queries/index.html +++ b/architecture/queries/index.html @@ -11,7 +11,7 @@ - + @@ -50,6 +50,6 @@

    Querying
    query CartTotalQuantityQuery($cartId: ID!): Float!

    [!NOTE] Query subscriptions are not supported yet

    -
    +
    \ No newline at end of file diff --git a/architecture/read-model/index.html b/architecture/read-model/index.html index bf9ec4358..547ee42b9 100644 --- a/architecture/read-model/index.html +++ b/architecture/read-model/index.html @@ -11,7 +11,7 @@ - + @@ -55,9 +55,10 @@

    @ReadModel
    export class UserReadModel {
    public constructor(readonly username: string, /* ...(other interesting fields from users)... */) {}

    @Projects(User, 'id')
    public static projectUser(entity: User, current?: UserReadModel): ProjectionResult<UserReadModel> {
    if (!current?.modified) {
    return ReadModelAction.Nothing
    }
    return new UserReadModel(...)
    }
    info

    Keeping the read model untouched higly recommended in favour of returning a new instance of the read model with the same data. This will not only prevent a new write operation in the database, making your application more efficient. It will also prevent an unnecessary update to be dispatched to any GrahpQL clients subscribed to that read model.

    Nested queries and calculated values using getters

    -

    You can use TypeScript getters in your read models to allow nested queries and/or return calculated values. You can write arbitrary code in a getter, but you will tipically query for related read model objects or generate a value computed based on the current read model instance or context. This greatly improves the potential of customizing your read model responses.

    +

    You can use TypeScript getters in your read models to allow nested queries and/or return calculated values. You can write arbitrary code in a getter, but you will typically query for related read model objects or generate a value computed based on the current read model instance or context. This greatly improves the potential of customizing your read model responses.

    +
    info

    Starting version 2.13, getters for values which are calculated using other properties of the read model need to be annotated with the @CalculatedField decorator and a list of those properties as dependencies.

    Here's an example of a getter in the UserReadModel class that returns all PostReadModels that belong to a specific UserReadModel:

    -
    @ReadModel
    export class UserReadModel {
    public constructor(readonly id: UUID, readonly name: string, private postIds: UUID[]) {}

    public get posts(): Promise<PostReadModel[]> {
    return this.postIds.map((postId) => Booster.readModel(PostReadModel)
    .filter({
    id: { eq: postId }
    })
    .search()
    }

    @Projects(User, 'id')
    public static projectUser(entity: User, current?: UserReadModel): ProjectionResult<UserReadModel> {
    return new UserReadModel(entity.id, entity.name, entity.postIds)
    }
    }
    +
    @ReadModel
    export class UserReadModel {
    public constructor(readonly id: UUID, readonly name: string, private postIds: UUID[]) {}

    @CalculatedField({ dependsOn: ['postIds'] })
    public get posts(): Promise<PostReadModel[]> {
    return this.postIds.map((postId) => Booster.readModel(PostReadModel)
    .filter({
    id: { eq: postId }
    })
    .search()
    }

    @Projects(User, 'id')
    public static projectUser(entity: User, current?: UserReadModel): ProjectionResult<UserReadModel> {
    return new UserReadModel(entity.id, entity.name, entity.postIds)
    }
    }

    As you can see, the getter posts uses the Booster.readModel(PostReadModel) method and filters it by the ids of the posts saved in the postIds private property. This allows you to retrieve all the PostReadModels that belong to a specific UserReadModel and include them as part of the GraphQL response.

    Also, you can see here a simple example of a getter called currentTime that returns the timestamp at the moment of the request:

    public get currentTime(): Date {
    return new Date()
    }
    @@ -95,6 +96,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/js/021264af.1ea64a64.js b/assets/js/021264af.1ea64a64.js deleted file mode 100644 index 38e048dc0..000000000 --- a/assets/js/021264af.1ea64a64.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.bb177d7c.js b/assets/js/021264af.bb177d7c.js new file mode 100644 index 000000000..b6d302f91 --- /dev/null +++ b/assets/js/021264af.bb177d7c.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.82165f7d.js b/assets/js/0350e44c.82165f7d.js deleted file mode 100644 index f6ec2f222..000000000 --- a/assets/js/0350e44c.82165f7d.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.8aa1e0f9.js b/assets/js/0350e44c.8aa1e0f9.js new file mode 100644 index 000000000..5a401ff79 --- /dev/null +++ b/assets/js/0350e44c.8aa1e0f9.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.169868c8.js b/assets/js/09ff0a1d.169868c8.js new file mode 100644 index 000000000..5c4a758a4 --- /dev/null +++ b/assets/js/09ff0a1d.169868c8.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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},{value:"Migrate to Booster version 2.6.0",id:"migrate-to-booster-version-260",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:"@SchemaMigration"})," 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:"@SchemaMigration(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:"@SchemaMigration(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": "3.1.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 Azure App Service Plan used to deploy the main function app. Setting the ",(0,o.jsx)(t.code,{children:"BOOSTER_AZURE_SERVICE_PLAN_BASIC"})," (default value false) environment variable to true will force the use of a basic service plan instead of the default consumption plan."]}),"\n",(0,o.jsx)(t.h2,{id:"migrate-to-booster-version-260",children:"Migrate to Booster version 2.6.0"}),"\n",(0,o.jsxs)(t.p,{children:["Booster 2.6.0 allows you to set the Azure Application Gateway SKU used. Setting the ",(0,o.jsx)(t.code,{children:"BOOSTER_USE_WAF"})," (default value false) environment variable to true will force the use of a WAF sku instead of the Standard sku."]})]})}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.2246d054.js b/assets/js/09ff0a1d.2246d054.js deleted file mode 100644 index 86a19fa22..000000000 --- a/assets/js/09ff0a1d.2246d054.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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},{value:"Migrate to Booster version 2.6.0",id:"migrate-to-booster-version-260",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:"@SchemaMigration"})," 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:"@SchemaMigration(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:"@SchemaMigration(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": "3.1.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 Azure App Service Plan used to deploy the main function app. Setting the ",(0,o.jsx)(t.code,{children:"BOOSTER_AZURE_SERVICE_PLAN_BASIC"})," (default value false) environment variable to true will force the use of a basic service plan instead of the default consumption plan."]}),"\n",(0,o.jsx)(t.h2,{id:"migrate-to-booster-version-260",children:"Migrate to Booster version 2.6.0"}),"\n",(0,o.jsxs)(t.p,{children:["Booster 2.6.0 allows you to set the Azure Application Gateway SKU used. Setting the ",(0,o.jsx)(t.code,{children:"BOOSTER_USE_WAF"})," (default value false) environment variable to true will force the use of a WAF sku instead of the Standard sku."]})]})}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.17d6c162.js b/assets/js/10057e71.17d6c162.js new file mode 100644 index 000000000..859d67697 --- /dev/null +++ b/assets/js/10057e71.17d6c162.js @@ -0,0 +1 @@ +"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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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/10057e71.9cb8d978.js b/assets/js/10057e71.9cb8d978.js deleted file mode 100644 index 868fc3eb1..000000000 --- a/assets/js/10057e71.9cb8d978.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.12ffbcd1.js b/assets/js/192d5973.12ffbcd1.js new file mode 100644 index 000000000..d6d1a71dc --- /dev/null +++ b/assets/js/192d5973.12ffbcd1.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.6b456963.js b/assets/js/192d5973.6b456963.js deleted file mode 100644 index 58aa99784..000000000 --- a/assets/js/192d5973.6b456963.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 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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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/1b08e8f8.1cc7cb96.js b/assets/js/1b08e8f8.1cc7cb96.js new file mode 100644 index 000000000..d580a5041 --- /dev/null +++ b/assets/js/1b08e8f8.1cc7cb96.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.c8d69eab.js b/assets/js/1b08e8f8.c8d69eab.js deleted file mode 100644 index 8cd9ade49..000000000 --- a/assets/js/1b08e8f8.c8d69eab.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.f0527717.js b/assets/js/1efc9436.f0527717.js new file mode 100644 index 000000000..0a6f0b63b --- /dev/null +++ b/assets/js/1efc9436.f0527717.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.f6c33ba5.js b/assets/js/1efc9436.f6c33ba5.js deleted file mode 100644 index 19f40463a..000000000 --- a/assets/js/1efc9436.f6c33ba5.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.3d388f4b.js b/assets/js/352d8b40.3d388f4b.js new file mode 100644 index 000000000..34edd91b2 --- /dev/null +++ b/assets/js/352d8b40.3d388f4b.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.f09e9f96.js b/assets/js/352d8b40.f09e9f96.js deleted file mode 100644 index 6530beff9..000000000 --- a/assets/js/352d8b40.f09e9f96.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.47d0e8c7.js b/assets/js/3c6e0dde.47d0e8c7.js new file mode 100644 index 000000000..ad6fe4bba --- /dev/null +++ b/assets/js/3c6e0dde.47d0e8c7.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.def98e3a.js b/assets/js/3c6e0dde.def98e3a.js deleted file mode 100644 index bda69711d..000000000 --- a/assets/js/3c6e0dde.def98e3a.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.04ee9783.js b/assets/js/3f8caafb.04ee9783.js deleted file mode 100644 index c18f770bf..000000000 --- a/assets/js/3f8caafb.04ee9783.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.e5fc4411.js b/assets/js/3f8caafb.e5fc4411.js new file mode 100644 index 000000000..587279e92 --- /dev/null +++ b/assets/js/3f8caafb.e5fc4411.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.fb9d888c.js b/assets/js/46b77955.fb9d888c.js new file mode 100644 index 000000000..0e14a17e3 --- /dev/null +++ b/assets/js/46b77955.fb9d888c.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 s=o(5893),n=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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 2024",sidebarPosition:0,frontMatter:{slug:"/"},sidebar:"docs",next:{title:"Introduction",permalink:"/introduction"}},u={},d=[];function l(t){const e={h1:"h1",...(0,n.a)(),...t.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(e.h1,{id:"ask-about-booster-framework",children:"Ask about Booster Framework"}),"\n",(0,s.jsx)(r.ZP,{})]})}function m(t={}){const{wrapper:e}={...(0,n.a)(),...t.components};return e?(0,s.jsx)(e,{...t,children:(0,s.jsx)(l,{...t})}):l(t)}},1151:(t,e,o)=>{o.d(e,{Z:()=>i,a:()=>a});var s=o(7294);const n={},r=s.createContext(n);function a(t){const e=s.useContext(r);return s.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(n):t.components||n:a(t.components),s.createElement(r.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/assets/js/46b77955.fbacbe85.js b/assets/js/46b77955.fbacbe85.js deleted file mode 100644 index 27f7775ff..000000000 --- a/assets/js/46b77955.fbacbe85.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.6ff07d05.js b/assets/js/4da0bd64.6ff07d05.js deleted file mode 100644 index 1fb08d12c..000000000 --- a/assets/js/4da0bd64.6ff07d05.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.70932fb2.js b/assets/js/4da0bd64.70932fb2.js new file mode 100644 index 000000000..87e9e6b99 --- /dev/null +++ b/assets/js/4da0bd64.70932fb2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[6038],{5277:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>i,contentTitle:()=>c,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>d});var o=n(5893),r=n(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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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 t={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)(t.h1,{id:"booster-instrumentation",children:"Booster instrumentation"}),"\n",(0,o.jsx)(t.h2,{id:"trace-decorator",children:"Trace Decorator"}),"\n",(0,o.jsxs)(t.p,{children:["The Trace Decorator is a ",(0,o.jsx)(t.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)(t.h3,{id:"usage",children:"Usage"}),"\n",(0,o.jsx)(t.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)(t.p,{children:"Here's an example of a custom tracer that logs trace events to the console:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.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)(t.p,{children:"You can then configure the tracer in your Booster application's configuration:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.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)(t.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)(t.p,{children:["Setting ",(0,o.jsx)(t.code,{children:"enableTraceNotification: true"})," would enable the trace for all actions. You can either disable them by setting it to ",(0,o.jsx)(t.code,{children:"false"})," or selectively enable only specific actions using an array of TraceActionTypes."]}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.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)(t.p,{children:"In this example, only DISPATCH_EVENT, MIGRATION_RUN and 'OTHER' actions will trigger trace notifications."}),"\n",(0,o.jsx)(t.h3,{id:"traceactiontypes",children:"TraceActionTypes"}),"\n",(0,o.jsx)(t.p,{children:"The TraceActionTypes enum defines all the traceable actions in Booster's core:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.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)(t.h3,{id:"traceinfo",children:"TraceInfo"}),"\n",(0,o.jsx)(t.p,{children:"The TraceInfo interface defines the data that is passed to the tracer's onBefore and onAfter methods:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.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)(t.p,{children:[(0,o.jsx)(t.code,{children:"className"})," and ",(0,o.jsx)(t.code,{children:"methodName"})," identify the function that is being traced."]}),"\n",(0,o.jsx)(t.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)(t.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)(t.p,{children:"Here's an example of how to use the Trace Decorator on a custom method:"}),"\n",(0,o.jsx)(t.pre,{children:(0,o.jsx)(t.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)(t.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)(t.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:t}={...(0,r.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:()=>c});var o=n(7294);const r={},a=o.createContext(r);function c(e){const t=o.useContext(a);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(r):e.components||r:c(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/55aa456f.1bdae15d.js b/assets/js/55aa456f.1bdae15d.js new file mode 100644 index 000000000..44a555911 --- /dev/null +++ b/assets/js/55aa456f.1bdae15d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkwebsite=self.webpackChunkwebsite||[]).push([[695],{4663:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>v,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var a=t(5893),r=t(1151),i=t(5163);const s={description:"Learn how to react to events and trigger side effects in Booster by defining event handlers."},l="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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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"}},d={},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},{value:"Creating a global event handler",id:"creating-a-global-event-handler",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"event-handler",children:"Event handler"}),"\n",(0,a.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,a.jsx)(n.h2,{id:"creating-an-event-handler",children:"Creating an event handler"}),"\n",(0,a.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,a.jsx)(i.Z,{children:(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-shell",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,a.jsxs)(n.p,{children:["This will generate a new file called ",(0,a.jsx)(n.code,{children:"handle-availability.ts"})," in the ",(0,a.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,a.jsx)(n.h2,{id:"declaring-an-event-handler",children:"Declaring an event handler"}),"\n",(0,a.jsxs)(n.p,{children:["In Booster, event handlers are classes decorated with the ",(0,a.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,a.jsx)(n.code,{children:"handle"})," method of the class. This ",(0,a.jsx)(n.code,{children:"handle"})," function will receive the event that triggered the handler."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.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,a.jsx)(n.h2,{id:"creating-an-event-handler-1",children:"Creating an event handler"}),"\n",(0,a.jsxs)(n.p,{children:["Event handlers can be easily created using the Booster CLI command ",(0,a.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,a.jsx)(i.Z,{children:(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,a.jsxs)(n.p,{children:["Once the creation is completed, there will be a new file in the event handlers directory ",(0,a.jsx)(n.code,{children:"/src/event-handlers/handle-availability.ts"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.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,a.jsx)(n.h2,{id:"registering-events-from-an-event-handler",children:"Registering events from an event handler"}),"\n",(0,a.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,a.jsxs)(n.p,{children:["In order to register new events, Booster injects the ",(0,a.jsx)(n.code,{children:"register"})," instance in the ",(0,a.jsx)(n.code,{children:"handle"})," method as a second parameter. This ",(0,a.jsx)(n.code,{children:"register"})," instance has a ",(0,a.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,a.jsx)(n.pre,{children:(0,a.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,a.jsx)(n.h2,{id:"reading-entities-from-event-handlers",children:"Reading entities from event handlers"}),"\n",(0,a.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,a.jsxs)(n.p,{children:["For that reason, Booster provides the ",(0,a.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,a.jsx)(n.code,{children:"Booster.entity"})," function, which will return information about the entity."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.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"})}),"\n",(0,a.jsx)(n.h2,{id:"creating-a-global-event-handler",children:"Creating a global event handler"}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"Booster"})," includes a ",(0,a.jsx)(n.code,{children:"Global event handler"}),". This feature allows you to react to any event that occurs within the system.\nBy annotating a class with the @GlobalEventHandler decorator, the handle method within that class will be automatically called for any event that is generated"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",children:"@GlobalEventHandler\nexport class GlobalHandler {\n public static async handle(event: EventInterface | NotificationInterface, register: Register): Promise {\n if (event instanceof LogEventReceived) {\n register.events(new LogEventReceivedTest(event.entityID(), event.value))\n }\n }\n"})})]})}function v(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},5163:(e,n,t)=>{t.d(n,{Z:()=>i});t(7294);const a={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=t(5893);function i(e){let{children:n}=e;return(0,r.jsxs)("div",{className:a.terminalWindow,children:[(0,r.jsx)("div",{className:a.terminalWindowHeader,children:(0,r.jsxs)("div",{className:a.buttons,children:[(0,r.jsx)("span",{className:a.dot,style:{background:"#f25f58"}}),(0,r.jsx)("span",{className:a.dot,style:{background:"#fbbe3c"}}),(0,r.jsx)("span",{className:a.dot,style:{background:"#58cb42"}})]})}),(0,r.jsx)("div",{className:a.terminalWindowBody,children:n})]})}},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>s});var a=t(7294);const r={},i=a.createContext(r);function s(e){const n=a.useContext(i);return a.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(r):e.components||r:s(e.components),a.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/55aa456f.6c5ab2ba.js b/assets/js/55aa456f.6c5ab2ba.js deleted file mode 100644 index b3580be44..000000000 --- a/assets/js/55aa456f.6c5ab2ba.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:()=>d,contentTitle:()=>l,default:()=>v,frontMatter:()=>s,metadata:()=>o,toc:()=>c});var a=t(5893),r=t(1151),i=t(5163);const s={description:"Learn how to react to events and trigger side effects in Booster by defining event handlers."},l="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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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"}},d={},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},{value:"Creating a global event handler",id:"creating-a-global-event-handler",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"event-handler",children:"Event handler"}),"\n",(0,a.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,a.jsx)(n.h2,{id:"creating-an-event-handler",children:"Creating an event handler"}),"\n",(0,a.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,a.jsx)(i.Z,{children:(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-shell",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,a.jsxs)(n.p,{children:["This will generate a new file called ",(0,a.jsx)(n.code,{children:"handle-availability.ts"})," in the ",(0,a.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,a.jsx)(n.h2,{id:"declaring-an-event-handler",children:"Declaring an event handler"}),"\n",(0,a.jsxs)(n.p,{children:["In Booster, event handlers are classes decorated with the ",(0,a.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,a.jsx)(n.code,{children:"handle"})," method of the class. This ",(0,a.jsx)(n.code,{children:"handle"})," function will receive the event that triggered the handler."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.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,a.jsx)(n.h2,{id:"creating-an-event-handler-1",children:"Creating an event handler"}),"\n",(0,a.jsxs)(n.p,{children:["Event handlers can be easily created using the Booster CLI command ",(0,a.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,a.jsx)(i.Z,{children:(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",children:"boost new:event-handler HandleAvailability --event StockMoved\n"})})}),"\n",(0,a.jsxs)(n.p,{children:["Once the creation is completed, there will be a new file in the event handlers directory ",(0,a.jsx)(n.code,{children:"/src/event-handlers/handle-availability.ts"}),"."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.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,a.jsx)(n.h2,{id:"registering-events-from-an-event-handler",children:"Registering events from an event handler"}),"\n",(0,a.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,a.jsxs)(n.p,{children:["In order to register new events, Booster injects the ",(0,a.jsx)(n.code,{children:"register"})," instance in the ",(0,a.jsx)(n.code,{children:"handle"})," method as a second parameter. This ",(0,a.jsx)(n.code,{children:"register"})," instance has a ",(0,a.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,a.jsx)(n.pre,{children:(0,a.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,a.jsx)(n.h2,{id:"reading-entities-from-event-handlers",children:"Reading entities from event handlers"}),"\n",(0,a.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,a.jsxs)(n.p,{children:["For that reason, Booster provides the ",(0,a.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,a.jsx)(n.code,{children:"Booster.entity"})," function, which will return information about the entity."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.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"})}),"\n",(0,a.jsx)(n.h2,{id:"creating-a-global-event-handler",children:"Creating a global event handler"}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsx)(n.strong,{children:"Booster"})," includes a ",(0,a.jsx)(n.code,{children:"Global event handler"}),". This feature allows you to react to any event that occurs within the system.\nBy annotating a class with the @GlobalEventHandler decorator, the handle method within that class will be automatically called for any event that is generated"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-typescript",children:"@GlobalEventHandler\nexport class GlobalHandler {\n public static async handle(event: EventInterface | NotificationInterface, register: Register): Promise {\n if (event instanceof LogEventReceived) {\n register.events(new LogEventReceivedTest(event.entityID(), event.value))\n }\n }\n"})})]})}function v(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},5163:(e,n,t)=>{t.d(n,{Z:()=>i});t(7294);const a={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=t(5893);function i(e){let{children:n}=e;return(0,r.jsxs)("div",{className:a.terminalWindow,children:[(0,r.jsx)("div",{className:a.terminalWindowHeader,children:(0,r.jsxs)("div",{className:a.buttons,children:[(0,r.jsx)("span",{className:a.dot,style:{background:"#f25f58"}}),(0,r.jsx)("span",{className:a.dot,style:{background:"#fbbe3c"}}),(0,r.jsx)("span",{className:a.dot,style:{background:"#58cb42"}})]})}),(0,r.jsx)("div",{className:a.terminalWindowBody,children:n})]})}},1151:(e,n,t)=>{t.d(n,{Z:()=>l,a:()=>s});var a=t(7294);const r={},i=a.createContext(r);function s(e){const n=a.useContext(i);return a.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(r):e.components||r:s(e.components),a.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/5b078add.4f2b0cf5.js b/assets/js/5b078add.4f2b0cf5.js deleted file mode 100644 index 20f659988..000000000 --- a/assets/js/5b078add.4f2b0cf5.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:"gonzalojaubert",lastUpdatedAt:1718121114,formattedLastUpdatedAt:"Jun 11, 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.5d3600f9.js b/assets/js/5b078add.5d3600f9.js new file mode 100644 index 000000000..6552e5ca4 --- /dev/null +++ b/assets/js/5b078add.5d3600f9.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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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.3f08a902.js b/assets/js/5e911e87.3f08a902.js new file mode 100644 index 000000000..16f3571f7 --- /dev/null +++ b/assets/js/5e911e87.3f08a902.js @@ -0,0 +1 @@ +"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:"Mario Castro Squella",lastUpdatedAt:1721404188,formattedLastUpdatedAt:"Jul 19, 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 - + diff --git a/blog/0001-purpose-and-guidelines/index.html b/blog/0001-purpose-and-guidelines/index.html index f10545cce..b12ad9d31 100644 --- a/blog/0001-purpose-and-guidelines/index.html +++ b/blog/0001-purpose-and-guidelines/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/0002-project-target/index.html b/blog/0002-project-target/index.html index 8b1dfacd0..5ec16a655 100644 --- a/blog/0002-project-target/index.html +++ b/blog/0002-project-target/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/0003-principles-of-design/index.html b/blog/0003-principles-of-design/index.html index c82a33168..cc85a7b6f 100644 --- a/blog/0003-principles-of-design/index.html +++ b/blog/0003-principles-of-design/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/0004-semantic-versioning/index.html b/blog/0004-semantic-versioning/index.html index d60f7fb41..bce923a47 100644 --- a/blog/0004-semantic-versioning/index.html +++ b/blog/0004-semantic-versioning/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/0005-agent-codebase/index.html b/blog/0005-agent-codebase/index.html index 35a9ecca7..4f4fa505c 100644 --- a/blog/0005-agent-codebase/index.html +++ b/blog/0005-agent-codebase/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/0006-remote-imports/index.html b/blog/0006-remote-imports/index.html index 771e4bafa..7315c26f3 100644 --- a/blog/0006-remote-imports/index.html +++ b/blog/0006-remote-imports/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/0007-components/index.html b/blog/0007-components/index.html index 786489554..ee40214d9 100644 --- a/blog/0007-components/index.html +++ b/blog/0007-components/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/archive/index.html b/blog/archive/index.html index 0c2ffedd6..73cdb9f1e 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -11,7 +11,7 @@ - + diff --git a/blog/index.html b/blog/index.html index 867f584f5..8e65b2e37 100644 --- a/blog/index.html +++ b/blog/index.html @@ -11,7 +11,7 @@ - + diff --git a/booster-cli/index.html b/booster-cli/index.html index 511623fc8..63f3794c4 100644 --- a/booster-cli/index.html +++ b/booster-cli/index.html @@ -11,7 +11,7 @@ - + @@ -25,6 +25,6 @@

    UsageOnce the installation is finished, you will have the boost command available in your terminal. You can run it to see the help message.

    tip

    You can also run boost --help to get the same output.

    Command Overview

    -
    CommandDescription
    new:projectCreates a new Booster project in a new directory
    new:commandCreates a new command in the project
    new:entityCreates a new entity in the project
    new:eventCreates a new event in the project
    new:event-handlerCreates a new event handler in the project
    new:read-modelCreates a new read model in the project
    new:scheduled-commandCreates a new scheduled command in the project
    start -e <environment>Starts the project in development mode
    buildBuilds the project
    deploy -e <environment>Deploys the project to the cloud
    nukeDeletes all the resources created by the deploy command
    +
    CommandDescription
    new:projectCreates a new Booster project in a new directory
    new:commandCreates a new command in the project
    new:entityCreates a new entity in the project
    new:eventCreates a new event in the project
    new:event-handlerCreates a new event handler in the project
    new:read-modelCreates a new read model in the project
    new:scheduled-commandCreates a new scheduled command in the project
    start -e <environment>Starts the project in development mode
    buildBuilds the project
    deploy -e <environment>Deploys the project to the cloud
    nukeDeletes all the resources created by the deploy command
    \ No newline at end of file diff --git a/category/features/index.html b/category/features/index.html index c98acfca2..88cd81867 100644 --- a/category/features/index.html +++ b/category/features/index.html @@ -11,7 +11,7 @@ - + diff --git a/category/getting-started/index.html b/category/getting-started/index.html index 9f02b2e01..1065634d1 100644 --- a/category/getting-started/index.html +++ b/category/getting-started/index.html @@ -11,7 +11,7 @@ - + diff --git a/category/going-deeper-with-booster/index.html b/category/going-deeper-with-booster/index.html index 3bd065f92..32daf0219 100644 --- a/category/going-deeper-with-booster/index.html +++ b/category/going-deeper-with-booster/index.html @@ -11,7 +11,7 @@ - + diff --git a/contributing/index.html b/contributing/index.html index f1952b290..fe36121c1 100644 --- a/contributing/index.html +++ b/contributing/index.html @@ -11,7 +11,7 @@ - + @@ -333,6 +333,6 @@

    Functional
    import { functionA, someConstantA } from 'module-a'
    import { ModuleB } from 'module-b'
    import { ObjectC } from 'object-c'

    functionA()
    ModuleB.functionB1()
    const obj = new ObjectC(someConstantA)

    Use const and let

    Default to const and immutable objects when possible, otherwise, use let.

    -
    // Good
    let a = 0
    const b = 3
    a = a + b

    // Less Good
    var c = 0
    let d = 3 // Never updated
    +
    // Good
    let a = 0
    const b = 3
    a = a + b

    // Less Good
    var c = 0
    let d = 3 // Never updated
    \ No newline at end of file diff --git a/features/error-handling/index.html b/features/error-handling/index.html index 6215ee6d8..e570239d2 100644 --- a/features/error-handling/index.html +++ b/features/error-handling/index.html @@ -11,7 +11,7 @@ - + @@ -47,6 +47,6 @@

    All errorsThis method receives the error that was thrown.

    Global error handler example

    You can implement all error handling functions in the same class. Here is an example of a global error handler that will handle all the errors mentioned above:

    -
    @GlobalErrorHandler()
    export class AppErrorHandler {
    public static async onCommandHandlerError(error: Error, command: CommandEnvelope): Promise<Error | undefined> {
    return error
    }

    public static async onScheduledCommandHandlerError(error: Error): Promise<Error | undefined> {
    return error
    }

    public static async onDispatchEventHandlerError(error: Error, eventInstance: EventInterface): Promise<Error | undefined> {
    return error
    }

    public static async onReducerError(
    error: Error,
    eventInstance: EventInterface,
    snapshotInstance: EntityInterface | null
    ): Promise<Error | undefined> {
    return error
    }

    public static async onProjectionError(
    error: Error,
    entity: EntityInterface,
    readModel: ReadModelInterface | undefined
    ): Promise<Error | undefined> {
    return error
    }

    public static async onError(error: Error | undefined): Promise<Error | undefined> {
    return error
    }
    }
    +
    @GlobalErrorHandler()
    export class AppErrorHandler {
    public static async onCommandHandlerError(error: Error, command: CommandEnvelope): Promise<Error | undefined> {
    return error
    }

    public static async onScheduledCommandHandlerError(error: Error): Promise<Error | undefined> {
    return error
    }

    public static async onDispatchEventHandlerError(error: Error, eventInstance: EventInterface): Promise<Error | undefined> {
    return error
    }

    public static async onReducerError(
    error: Error,
    eventInstance: EventInterface,
    snapshotInstance: EntityInterface | null
    ): Promise<Error | undefined> {
    return error
    }

    public static async onProjectionError(
    error: Error,
    entity: EntityInterface,
    readModel: ReadModelInterface | undefined
    ): Promise<Error | undefined> {
    return error
    }

    public static async onError(error: Error | undefined): Promise<Error | undefined> {
    return error
    }
    }
    \ No newline at end of file diff --git a/features/event-stream/index.html b/features/event-stream/index.html index 863cb4fde..f3a24a172 100644 --- a/features/event-stream/index.html +++ b/features/event-stream/index.html @@ -11,7 +11,7 @@ - + @@ -26,6 +26,6 @@

    @Entity({
    authorizeReadEvents: 'all', // Anyone can read any Cart's event
    })
    export class Cart {
    public constructor(
    readonly id: UUID,
    readonly cartItems: Array<CartItem>,
    public shippingAddress?: Address,
    public checks = 0
    ) {}
    // <reducers...>
    }
    note

    Be careful when exposing events data, as this data is likely to hold internal system state. Pay special attention when authorizing public access with the 'all' option, it's always recommended to look for alternate solutions that limit access.

    -

    To read more about how to restrict the access to the event stream API, check out the authorization guide.

    +

    To read more about how to restrict the access to the event stream API, check out the authorization guide.

    \ No newline at end of file diff --git a/features/logging/index.html b/features/logging/index.html index 4a5e07ba1..bc0760520 100644 --- a/features/logging/index.html +++ b/features/logging/index.html @@ -11,7 +11,7 @@ - + @@ -36,6 +36,6 @@

    Us
    @Command({
    authorize: [User],
    })
    export class UpdateShippingAddress {
    public constructor(readonly cartId: UUID, readonly address: Address) {}

    public static async handle(command: UpdateShippingAddress, register: Register): Promise<void> {
    const logger = getLogger(Booster.config, 'UpdateShippingCommand#handler', 'MyApp')
    logger.debug(`User ${register.currentUser?.username} changed shipping address for cart ${command.cartId}: ${JSON.stringify(command.address}`)
    register.events(new ShippingAddressUpdated(command.cartId, command.address))
    }
    }

    When a UpdateShippingAddress command is handled, it wil log messages that look like the following:

    [MyApp]|UpdateShippingCommand#handler: User buyer42 changed shipping address for cart 314: { street: '13th rue del percebe', number: 6, ... }
    -
    info

    Using the configured Booster logger is not mandatory for your application, but it might be convenient to centralize your logs and this is a standard way to do it.

    +
    info

    Using the configured Booster logger is not mandatory for your application, but it might be convenient to centralize your logs and this is a standard way to do it.

    \ No newline at end of file diff --git a/features/schedule-actions/index.html b/features/schedule-actions/index.html index 8bc608891..dafe83691 100644 --- a/features/schedule-actions/index.html +++ b/features/schedule-actions/index.html @@ -11,7 +11,7 @@ - + @@ -33,6 +33,6 @@

    Scheduled

    By default, if no paramaters are passed, the scheduled command will not be triggered.

    Creating a scheduled command

    The preferred way to create a scheduled command is by using the generator, e.g.

    -
    boost new:scheduled-command CheckCartCount
    +
    boost new:scheduled-command CheckCartCount
    \ No newline at end of file diff --git a/frequently-asked-questions/index.html b/frequently-asked-questions/index.html index 617965fd8..e90210c57 100644 --- a/frequently-asked-questions/index.html +++ b/frequently-asked-questions/index.html @@ -11,7 +11,7 @@ - + @@ -20,6 +20,6 @@

    When you deploy a Booster application to AWS, an S3 bucket needs to be created to upload the application code. Booster names that bucket using your application name as a prefix. In AWS, bucket names must be unique globally, so if there is another bucket in the world with exactly the same name as the one generated for your application, you will get this error.

    The solution is to change your application name in the configuration file so that the bucket name is unique.

    2.- I tried following the video guide but the function Booster.fetchEntitySnapshot is not found in BoostApp.

    -

    The function Booster.fetchEntitySnapshot was renamed to Booster.entity, so please replace it when following old tutorials.

    +

    The function Booster.fetchEntitySnapshot was renamed to Booster.entity, so please replace it when following old tutorials.

    \ No newline at end of file diff --git a/getting-started/coding/index.html b/getting-started/coding/index.html index 270a51afc..e85794cc3 100644 --- a/getting-started/coding/index.html +++ b/getting-started/coding/index.html @@ -11,7 +11,7 @@ - + @@ -228,6 +228,6 @@

    this GitHub repo.

    All the guides and examples

    -

    Check out the example apps repository to see Booster in use.

    +

    Check out the example apps repository to see Booster in use.

    \ No newline at end of file diff --git a/getting-started/installation/index.html b/getting-started/installation/index.html index e8847b42a..aa84622cf 100644 --- a/getting-started/installation/index.html +++ b/getting-started/installation/index.html @@ -11,7 +11,7 @@ - + @@ -64,6 +64,6 @@

    I something like

    boost version

    @boostercloud/cli/0.16.1 darwin-x64 node-v14.14.0

    -
    + \ No newline at end of file diff --git a/going-deeper/azure-scale/index.html b/going-deeper/azure-scale/index.html index 5afbc41af..d9205b1fd 100644 --- a/going-deeper/azure-scale/index.html +++ b/going-deeper/azure-scale/index.html @@ -11,7 +11,7 @@ - + @@ -50,6 +50,6 @@

    Recommendati

    From the Azure documentation:

    Dynamically adding partitions isn't recommended. While the existing data preserves ordering, partition hashing will be broken for messages hashed after the partition count changes due to addition of partitions.

    -
    +
    \ No newline at end of file diff --git a/going-deeper/custom-providers/index.html b/going-deeper/custom-providers/index.html index e26006136..01f3c297f 100644 --- a/going-deeper/custom-providers/index.html +++ b/going-deeper/custom-providers/index.html @@ -11,7 +11,7 @@ - + @@ -104,6 +104,6 @@

    Discord where some community members will can help you. - + \ No newline at end of file diff --git a/going-deeper/custom-templates/index.html b/going-deeper/custom-templates/index.html index f0852d536..9343b468e 100644 --- a/going-deeper/custom-templates/index.html +++ b/going-deeper/custom-templates/index.html @@ -11,7 +11,7 @@ - + @@ -41,6 +41,6 @@

    QAHow do I change what new:project command generates? -
    I have another question!

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

    +
    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 751502ba0..b2778378a 100644 --- a/going-deeper/data-migrations/index.html +++ b/going-deeper/data-migrations/index.html @@ -11,7 +11,7 @@ - + @@ -64,6 +64,6 @@

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

    Booster 2.3.0 allows you to set the Azure App Service Plan used to deploy the main function app. Setting the BOOSTER_AZURE_SERVICE_PLAN_BASIC (default value false) environment variable to true will force the use of a basic service plan instead of the default consumption plan.

    Migrate to Booster version 2.6.0

    -

    Booster 2.6.0 allows you to set the Azure Application Gateway SKU used. Setting the BOOSTER_USE_WAF (default value false) environment variable to true will force the use of a WAF sku instead of the Standard sku.

    +

    Booster 2.6.0 allows you to set the Azure Application Gateway SKU used. Setting the BOOSTER_USE_WAF (default value false) environment variable to true will force the use of a WAF sku instead of the Standard sku.

    \ No newline at end of file diff --git a/going-deeper/environment-configuration/index.html b/going-deeper/environment-configuration/index.html index d1737aa93..c1906ace7 100644 --- a/going-deeper/environment-configuration/index.html +++ b/going-deeper/environment-configuration/index.html @@ -11,7 +11,7 @@ - + @@ -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 696196c34..51b8bf7d0 100644 --- a/going-deeper/framework-packages/index.html +++ b/going-deeper/framework-packages/index.html @@ -11,7 +11,7 @@ - + @@ -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 644ad75c7..9c7b624e8 100644 --- a/going-deeper/health/sensor-health/index.html +++ b/going-deeper/health/sensor-health/index.html @@ -11,7 +11,7 @@ - + @@ -252,6 +252,6 @@

    Examplehttps://your-application-url/sensor/health/database, children will not be visible

    └── database

    but you can access to them using the component url https://your-application-url/sensor/health/database/events

    -
    └── events
    +
    └── events
    \ No newline at end of file diff --git a/going-deeper/infrastructure-providers/index.html b/going-deeper/infrastructure-providers/index.html index e07978e8f..86bdb0a45 100644 --- a/going-deeper/infrastructure-providers/index.html +++ b/going-deeper/infrastructure-providers/index.html @@ -11,7 +11,7 @@ - + @@ -193,6 +193,6 @@

    +

    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 78eb1fae1..28dfd4176 100644 --- a/going-deeper/instrumentation/index.html +++ b/going-deeper/instrumentation/index.html @@ -11,7 +11,7 @@ - + @@ -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 1c6dc1978..f865c8481 100644 --- a/going-deeper/register/index.html +++ b/going-deeper/register/index.html @@ -11,7 +11,7 @@ - + @@ -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 d93e679d0..02d418ed3 100644 --- a/going-deeper/rockets/index.html +++ b/going-deeper/rockets/index.html @@ -11,7 +11,7 @@ - + @@ -117,6 +117,6 @@

    Namin

    Booster Rockets list

    Here you can check out the official Booster Rockets developed at this time:

    -
    +
    \ 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 7bdb37739..4ba65a44a 100644 --- a/going-deeper/rockets/rocket-backup-booster/index.html +++ b/going-deeper/rockets/rocket-backup-booster/index.html @@ -11,7 +11,7 @@ - + @@ -23,6 +23,6 @@

    UsageInstall this package as a dev dependency in your Booster project:

    npm install --save-dev @boostercloud/rocket-backup-aws-infrastructure

    In your Booster config file, pass a RocketDescriptor array to the AWS' Provider initializer configuring the backup rocket:

    -
    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',
    }
    }
    }])
    })
    +
    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 2ad339d3a..f272917ff 100644 --- a/going-deeper/rockets/rocket-file-uploads/index.html +++ b/going-deeper/rockets/rocket-file-uploads/index.html @@ -11,7 +11,7 @@ - + @@ -60,6 +60,6 @@

    TODOsOptional storage deletion when unmounting the stack.
  • Optional events, in case you don't want to store that information in the events-store.
  • When deleting a file, save a deletion event in the events-store. Only uploads are stored at the moment.
  • -
    + \ 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 ad869f6f8..17909949a 100644 --- a/going-deeper/rockets/rocket-static-sites/index.html +++ b/going-deeper/rockets/rocket-static-sites/index.html @@ -11,7 +11,7 @@ - + @@ -22,6 +22,6 @@

    UsageInstall 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)

    npm install --save-dev @boostercloud/rocket-static-sites-aws-infrastructure

    In your Booster config file, pass a RocketDescriptor in the config.rockets array to configuring the static site rocket:

    -
    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
    }
    },
    ]
    })
    +
    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 fdc07e3b3..826bcd633 100644 --- a/going-deeper/rockets/rocket-webhook/index.html +++ b/going-deeper/rockets/rocket-webhook/index.html @@ -11,7 +11,7 @@ - + @@ -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 dd7c8e06c..8cd7dbcaf 100644 --- a/going-deeper/sensor/index.html +++ b/going-deeper/sensor/index.html @@ -11,11 +11,11 @@ - + +
    \ No newline at end of file diff --git a/going-deeper/testing/index.html b/going-deeper/testing/index.html index 4e62a7718..3fc4f2148 100644 --- a/going-deeper/testing/index.html +++ b/going-deeper/testing/index.html @@ -11,7 +11,7 @@ - + @@ -41,6 +41,6 @@

  • Tests to ensure that different packages integrate as expected with each other.
  • Tests to ensure that a Booster application behaves as expected when it is hit by a client (a GraphQL client).
  • Tests to ensure that the application behaves in the same way no matter what provider is selected.
  • - + \ No newline at end of file diff --git a/going-deeper/touch-entities/index.html b/going-deeper/touch-entities/index.html index 78e14ac2a..088210669 100644 --- a/going-deeper/touch-entities/index.html +++ b/going-deeper/touch-entities/index.html @@ -11,7 +11,7 @@ - + @@ -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 756445162..6130ee132 100644 --- a/graphql/index.html +++ b/graphql/index.html @@ -11,7 +11,7 @@ - + @@ -350,6 +350,6 @@

    here.

    -
    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.

    +
    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 c5f8e77ff..f682cd163 100644 --- a/index.html +++ b/index.html @@ -11,11 +11,11 @@ - +

    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 beff3a5fb..bb7986aaa 100644 --- a/introduction/index.html +++ b/introduction/index.html @@ -11,7 +11,7 @@ - + @@ -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/search/index.html b/search/index.html index 8202ba1b0..4c7f5fdda 100644 --- a/search/index.html +++ b/search/index.html @@ -11,7 +11,7 @@ - + diff --git a/security/authentication/index.html b/security/authentication/index.html index fca33704a..439faa174 100644 --- a/security/authentication/index.html +++ b/security/authentication/index.html @@ -11,7 +11,7 @@ - + @@ -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 6c3130b44..db2ef0e24 100644 --- a/security/authorization/index.html +++ b/security/authorization/index.html @@ -11,7 +11,7 @@ - + @@ -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 9a56d67bf..f75238bec 100644 --- a/security/security/index.html +++ b/security/security/index.html @@ -11,7 +11,7 @@ - + @@ -20,6 +20,6 @@ Likewise, you can use the claims included in these tokens to authorize access to commands or read models by using the provided simple role-based authorization or writing your own authorizer functions.

    -
    +
    \ No newline at end of file