Skip to content

Activity Feed Architecture & Files

tylowe-labs edited this page Apr 20, 2023 · 26 revisions

The objective of this guide is to explain the architecture for the newly added Activity Feed feature as well as walk through the files that were changed and how they interact. After reading this guide, you should be able to understand which files to edit to make changes as well as how the InvolveMINT repository itself is built. Although the information here pertains to the Activity Feed feature, the files changed and the patterns can be applied generally to add features to the InvolveMINT repository and are a helpful starting place for any developer.

The code for the Activity Feed can be found on the feature/activity-feed branch although in the near future it may be merged into the main

Prerequisites

  • Ensure that you are able to pull the codebase down and run it. Follow the instructions here.
  • It's likely also helpful to have completed the guide here, so that you have a basic understanding of the InvolveMINT architecture, files, and system interactions.

Activity Feed Architecture + Files Explained

To implement a substantial full-stack change to the InvolveMINT repository--one that adds new persistent data and has new pages for users to view--there are five key areas of the repository that need to be architected.

Domain ℹ️

As indicated by the name, files within the shared/domain/* project define the data domains for each of the objects used within the InvolveMINT application. By 'objects' I mean any sort of persistent data that is stored and used by the application (such as an Activity Post 😄). This is the location where developers need to edit/create files to define the general shape of a data object, the orchestration (APIs) between the client-server and their inputs/outputs, queries for how data is fetched from storage, generic logic, and more. The domain should be the first addition when adding a new feature and follows the DRY principle (Don't repeat yourself), because the defined data domains are reused in all other areas via import {...} from '@involvemint/shared/domain'.

  • Activity Post Orchestration Interface => Orchestrations at InvolveMINT are VERY IMPORTANT. They define the methods that connect the client-server together by specifying the names of the methods, their return values, and the DTO (input arguments). The IActivityPostOrchestration defines the interface for the orchestrations that Activity Posts will have; however, both the server and client must implement the interface in order to allow client to call the server and receive the requested data as required.

    • Ex:
      • get => defines the interface for a get method which enables the client to call the server (using the method and passing a DTO input) to fetch an individual Activity Post.
  • Activity Post DTOs => DTOs are a data structure that defines the incoming arguments for an orchestration call (1+ arguments); The Activity Post DTOs do this for the IActivityPostOrchestration calls ⬆️

    • Ex:
      • GetActivityPostDto => Defines the arguments for the get IActivityPostOrchestration call. Simply takes in a post ID string so that the server is able to fetch the post from the DB.
  • Activity Post Model => ActivityPost defines the interface for the Activity Post modal. It explicitly lays out what the fields are for an Activity Post, their values, and their relationship to other models in the InvolveMINT repository.

  • Activity Post Queries => Queries are consumed by orchestration calls on the Client-side and they are used to tell services which fields for an entity should be returned ('true' value on the field). For an example of how the queries are used see the code for PostEffects here and [here]https://github.com/involveMINT/iMPublic/blob/ac819468e84a2b233fd26b16dcb2bd4ef97bb2c2/libs/client/shared/data-access/src/lib/%2Bstate/activity-posts/activity-posts.effects.ts#L59-L62().

    • Ex:
      • ActivityPostQuery => Generic query for an ActivityPost which returns a subset of the fields in an ActivityPost.
      • ActivityFeedQuery => Generic query to fetch a paginated list of ActivityPosts where the limit per page is 10

Client State Management 🌀

These files are responsible for handling the management of the application state on the client side--basically, the data that Angular components use to render a view for the user. To handle state management, the InvolveMINT repository uses the powerful NgRx which requires knowledge of observables streams. The NgRx files are generally located in the client/**data-access/* folders. By editing/creating 'actions', 'effects', 'reducers', and 'selectors' (NgRx components) developers are able to define what happens based on state changes, how things get updated, and how they can be displayed to users. The NgRx state management is hugely important for the frontend because files in the 'Client Angular Components' area subscribe to the defined 'Client State Management' to know which data to show to users and when to update. It effectively abstracts away the frontend state management and allows Angular Components to focus on just rendering the views. It's also important to know that the 'Client State Management' relies on the 'Client <--> Server Orchestration' area because a piece of state management is fetching data from the server/backend which is when the orchestrations are utilized.

  • Activity Post Actions => An 'action' is a component of Angular state management that sends out a signal that something happened or needs to be done. They have similarities to functions in that releasing/invoking an action on the state management lifecycle will invoke some background action in the system. With actions you specify the name of the action (so others can listen for it) and the values that are passed along with that action.

    • Examples:
      • loadPosts => signals that Activity Posts need to be loaded + presents params.
      • loadPostsSuccess => signals that Activity Posts have been fetched successfully + presents outputs.
  • Activity Post Effects => An 'effect' is a component of Angular state management that listens for actions and perform corresponding effects on the system. It is where you define the logic for actions. For the most part, we use effects as a way to fetch data from the backend and then prepare it to be presented to the user interface.

    • Ex:
      • loadPosts$ => listens for loadPosts action via ofType(..) and then fetches the next page of Activity Posts and passes on a either a loadPostsSuccess or loadPostsError action with appropriate values into the state management lifecycle.
  • Activity Post Reducer => A 'reducer' is a component of Angular state management that listens for actions and performs state change to reduce the incoming data from the action into the existing state. There is a 'PostState' which defines the structure of the state for the reducer, and an 'initialState' which defines the initial values for the state (which is inputted to reduce new state when actions passed).

    • Ex:
      • loadPostsSuccess => listens for the loadPostsSuccess action (outputted by PostEffects) and creates a new PostState by taking the existing state and the values passed by the loadPostsSuccess (posts, page, limit) and merging them. This specific reducer does so by adding the new posts to the existing state posts, updating page to be the value of the action passed page, and checking the length of posts list returned.
  • Activity Post Selector => A 'selector' provides a memoized view of the Post State. It allows Angular components to pull from a memoized view of the state that can be updated as the data morphs.

    • Ex:
      • getPosts => provides a memoized view of the Post State by revealing all the current posts, pages loaded, limit, and if all pages are loaded.
  • User Facade -- Activity Posts Additions => A 'facade' in the InvolveMINT repository is simply a wrapper around the NgRx components specified above ⬆️ that can be used in Angular components to make managing the state easier. Generally, there are dispatchers to dispatch 'actions' into the state lifecycle, selectors to allow components to easily access defined 'selectors', and action listeners that trigger when a specific action is observed. The highlighted code here shows how the UserFacade is utilized within a component to manage state.

    • Ex:
      • posts.dispatchers.loadPosts => selects from the state store the current posts, then dispatches a loadPosts actions to fetch the next page of posts (which will be picked up by 'effects' and passed along).
      • posts.selectors.posts$ => selects from the state store the current posts using the getPosts selector and performs an initial load of Posts by dispatching a loadPosts if nothing loaded yet; components can pipe/subscribe to this method to receive updates when the state changes.

Client Angular Components 💻

These files are responsible for rendering the user view and are what most people would consider being 'frontend' development. As such, this area is where developers need to edit/create files to define how users will see and interact with the InvolveMINT application as well as the data that will be used and the logic that occurs. As indicated by the name, the InvolveMINT repository uses Angular components to handle business logic and rendering of frontend views. Angular components that render InvolveMINT data to users utilize the code created in the 'Client State Management' area by subscribing to a 'selector' which fetches data from the state management to the component. The Angular files are generally located in the client/**shell/* folders. It's also important to note that generally Angular components reside within a parent @NgModule where the imports are defined and the component needs to be declared. The Angular component itself has a .ts, .html, and .scss file that work together to create the business logic, view, and styles respectively.

  • Activity Feed Component => The first thing you will notice is the State interface defined at the start. Defining a 'State' interface for an Angular component and extending the StatefulComponent<State> allows the component to know what the state being tracked is and dynamically update/re-render the component when the underlying state is updated (via this.updateState(...)). The Activity Posts Component state tracks the currently loaded Activity Posts, the digest posts (notifications), and if the state has been loaded. The ActivityFeedComponent itself is the primary component that works in conjunction with activityposts.component.html to provide the Activity Post Feed view to users. It is a stateful component (see state interface), which tracks the Activity Posts loaded in the state manager using the posts$ selector view. It provides an infinite scrolling ability to users that will continuously fetch and display more posts as users scroll down until. This component handles mostly the tracking/re-rending of post data and user requests, the rendering of individual posts data is handled mostly by the post.component.

  • Activity Post Component => The component--PostComponent--responsible for the rendering/view of actual Activity Posts to users and providing like/unlike and ability to pull up comment functionality. The post to be rendered needs to be passed to the component via a post input value. It is NOT a stateful component and it needs to be used within other stateful components that can track and re-render the component when necessary (see activityposts.component.ts/html for example).

  • Notification Digest Modal Component => The ModalDigestComponent is responsible for rendering a modal to view the Activity Feed notification digest for a user and it is opened when a button is clicked in ActivityFeedComponent. The modal requires a digestPosts input which is a list of the posts with recent activity to generate digest notifications from. The digest modal also provides the ability to click on a notification which will fetch and display the corresponding post using the UserFacade and ModalPostComponent. On close, the component also returns the digest posts outstanding (not viewed), so the ActivityFeedComponent can update its state.

  • Activity Post Modal Component => The ModalPostComponent is responsible for tracking and rendering a modal to view a single Activity Post. As such, its State interface tracks a single post. Currently, it is used to view a Post attached to a notification when clicked in ModalDigestComponent. The component tracks the Activity Post using the getPost(...) selector view and renders the Activity Post within the component using the activity post. component. The modal also requires a post input which it uses to decipher which post to track.

Client <--> Server Orchestration 📞

Client-Server orchestration is all about getting the client and server to communicate. It may not be clear from the mono-repository, but the 'client' and 'server' projects are actually deployed in different containers in production and require a means to communicate over the network in order to send/receive data. To solve this, the framework that is used is called @orcha and was built by a previous InvolveMINT developer. In essence, you create a generic 'orchestration interface' (in domain) that is implemented in both the Client and Server to create an RPC networking connection automatically. All you need to know about an RPC connection is that it allows you to fetch data across the network (client -> server -> client) by doing a simple method call on the Client orchestration and passing the appropriate values. Setting up the orchestration correctly is very important because the 'Client State Management' uses the orchestration connection to fetch/send data to the server based on user interactions and on the other hand the 'Server Application + Domain Services' relies on the orchestration connection to know when they need to execute server application/domain logic. The orchestration itself relies on the 'Domain' area because that is where the interface (methods to fetch/send data) is defined.

  • Activity Post Orchestration Interface => Orchestrations at InvolveMINT are VERY IMPORTANT. They define the methods that connect the client-server together by specifying the names of the methods, their return values, and the DTO (input arguments). The IActivityPostOrchestration defines the interface for the orchestrations that Activity Posts will have; however, both the server and client must implement the interface in order to allow client to call the server and receive the requested data as required.

    • Ex:
      • get => defines the interface for a get method which enables the client to call the server (using the method and passing a DTO input) to fetch an individual Activity Post.
  • Client -- Activity Post Orchestration => Implements each of the orchestration methods defined in IActivityPostOrchestration ⬆️ for the client. Mostly a boilerplate file that registers the interface methods and the inputs, outputs, and orchestration calls to the implementation fields. Once created, however, the Client can use the ActivityPostOrchestration object and execute calls by simply calling the method and passing any appropriate DTOs/inputs. For an example, observe the use of ActivityPostOrchestration in PostEffects or here as a common effect is to fetch data from a backend service.

  • Server -- Activity Post Orchestration => Implements each of the orchestration methods defined in IActivityPostOrchestration ⬆️ for the server. The business logic for the method calls is handled by the ActivityPostService which gets forwarded the method arguments, executes application logic, then returns the appropriate values back.

Server Application + Domain Services 🧮

Services are the heart of the Server (whose main job is to persist data). Domain services on the Server generally take the data definitions defined in the 'Domain' area and then implement them so that there is a table in the database that can store information and provides a straightforward ORM (Object Relational Mapping) for easy/programmatic access to the data. Application services generally get forwarded RPC calls from the 'Client <--> Orchestration' area and are where the developers need to put the server logic that should be executed based on the orchestration call.

  • Activity Post Service => A 'service' implements the backend server application logic required for orchestration methods defined in orchestration interfaces such as IActivityPostOrchestration; however, they can also be standalone service that provides some useful utility (such as AuthService). They are responsible for validating inputs, performing validation checks, updating the databases as required, and returning outputs. They consume repositories (databases) and other services to implement the logic required. ActivityPostService is used in the Server implementation of IActivityPostOrchestration to wrap all the calls and provides the application logic for all orchestration calls.

    • Ex:
      • like(...) => Function that implements logic to add a like to an activity post... 1) validates user 2) checks user already liked 3) adds a record to like table 4) updates like count and returns the like post
  • Activity Post Entity => Implements the ActivityPost modal defined in the domain. ActivityPostEntity specifies the table name and field values/relationships with 'typeorm' notations s.t. 'typeorm' can map a post to an actual record in the database ('entity') and provide ORM functionality.

  • Activity Post Repository => ActivityPostRepository Uses the ActivityPostEntity ⬆️ and ActivityPost model to instantiate a repository (basically a table in a database to store the values). Once instantiated, these repositories are then commonly used in backend services to execute server logic on orchestration calls and update database values. For example, ActivityPostRepository is used in ActivityPostService.