Chapter 8: Discussion API. Post API. Websockets for Discussion and Post.
We keep our book up to date with recent libraries and packages.
The section below is a preview of SaaS Boilerplate Book. To read the full text, you will need to purchase the book.
In Chapter 8, you will start with the codebase in the 8-begin folder of our saas repo and end up with the codebase in the 8-end folder.
We will cover the following topics in this chapter:
Discussion API - Model and static methods - Discussion - Express routes - Discussion - API methods - Discussion - Data store - Discussion - Updatingn Team data store - Discussion - Discussion page - Discussion-specific components - Common components for Discussion API - Testing Discussion API without POST API
Post API - Model and static methods - Post - Express routes - Post - API methods - Post - Data store and store methods - Post - Updating DiscussionPageComp page and CreateDiscussionForm - Post-specific components - Testing Post API
Websockets for Discussion and Post - API server - API server - setupSockets method - API server - server-side websocket methods - APP client - Testing websockets
Discussion API link
In Chapter 7, we wrote code that redirects an end user to the YourSettings
page after accepting an invitation to join a team. We mentioned that later on we will change the redirect destination, since it makes little sense. As we keep working on our SaaS boilerplate, the question remains - what do end users do with our application? You, as a software developer and business owner, should answer this question early. What problem does your SaaS product solve? Who will pay for your product to solve this problem?
Since our SaaS boilerplate is a "boilerplate" after all, we will build a simple feature: Discussions. End users will discuss anything they want by creating a Discussion. A Discussion has participants and consists of Posts. Any member of a team (Team Leader or Team Member) will be able to create a new Discussion, name that Discussion, select participants from the team's members, and create a very first Post inside this new Discussion.
At the end of this section, we will redirect an end user who accepted a team invitation to the Discussion
page instead of the YourSettings
page. The Discussion
page that we build in this chapter, after we are done with Post-related components, will look like this:
Note that the page's route contains /team/:teamSlug/discussions/:discussionSlug
, similar to the TeamSettings
page. Recall how we wrote our Express route for the TeamSettings
page:
server.get('/team/:teamSlug/team-settings', (req, res) => { const { teamSlug } = req.params; app.render(req, res, '/team-settings', { teamSlug }); });
We write a similar Express route for the Discussion
page. We need to remember to add this route to our APP
server:
server.get('/team/:teamSlug/discussions/:discussionSlug', (req, res) => {
const { teamSlug, discussionSlug } = req.params;
app.render(req, res, '/discussion', { teamSlug, discussionSlug });
});
Here is a highlight of most (but not all) components we will build for our Discussion API:
No changes to the API infrastructure - it's the same as the previous internal API infrastructures (for example, Team):
Model and static methods - Discussion link
We start building Discussion API with our API
project (server-only code). Previously, we built Team API starting with our API
server and Invitation API starting with our APP
server/client. It's really up to you where to start when building internal API infrastructures. We, as a small team that released and continuously develops our SaaS product, Async, typically start building internal APIs from server-only code (API
project).
In this subsection, we will define a new data model called Discussion
. This task should be easy to complete by this point in the book, since we alreay defined four other data models: - User - EmailTemplate - Team - Invitation
As with as any other data model in this book, we follow these steps when defining a new model using Mongoose: - defining Schema - defining interface for Document - defining interface for Model - defining static methods for class that extends Model - exporting Document - define and export Model using Schema with static methods and the above interfaces - adding required imports, settings
Let's go through the above blueprint to define a new data model called Discussion
.
- Schema has a
createdUserId
property - a user id of the Discussion's creator. It also hasmemberIds
- an array of user ids who are participants of the Discussion. The Discussion's creator selects users from all team members to become that Discussion's participants. Only participants can see the Discussion and create new Posts within it. Schema also has ateamId
property, because users create Discussions within a specific Team. In other words, members of one Team cannot see Discussions of another Team. An end user will access theDiscussion
page using a URL that contains/team/:teamSlug/discussions/:discussionSlug
. There could be two Discussions with the samediscussionSlug
, but they have to be in two different Teams. When an end user loads theDiscussion
page, we will show list of all Discussions within the Team using theteamId
property. Below, we will define a static method,getList
, that retrieves and returns a list of all Discussions for a given Team.
In addition to the above properties, there are common properties such as name
, slug
, and createdAt
:
const mongoSchema = new mongoose.Schema({
createdUserId: {
type: String,
required: true,
},
teamId: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
slug: {
type: String,
required: true,
},
memberIds: [
{
type: String,
},
],
createdAt: {
type: Date,
required: true,
default: Date.now,
},
});
interface Document is for defining data types for properties we specified in Schema:
interface DiscussionDocument extends mongoose.Document { createdUserId: string; teamId: string; name: string; slug: string; memberIds: string[]; createdAt: Date; }
interface Model is for defining data types for methods' arguments and return values. We will have five static methods:
getList
,add
,edit
,delete
, andcheckPermissionAndGetTeam
:interface DiscussionModel extends mongoose.Model<DiscussionDocument> { getList({ userId, teamId, }: { userId: string; teamId: string; }): Promise<{ discussions: DiscussionDocument[] }>; add({ name, userId, teamId, memberIds, }: { name: string; userId: string; teamId: string; memberIds: string[]; }): Promise<DiscussionDocument>; edit({ userId, id, name, memberIds, }: { userId: string; id: string; name: string; memberIds: string[]; }): Promise<DiscussionDocument>; delete({ userId, id }: { userId: string; id: string }): Promise<{ teamId: string }>; checkPermissionAndGetTeam({ userId, teamId, memberIds, }: { userId: string; teamId: string; memberIds: string[]; }): Promise<TeamDocument>; }
Static method
getList
uses the Mongoose API methodfind
to find and return an array of Discussion objects that have a matchingteamId
property:public static async getList({ userId, teamId }) { await this.checkPermissionAndGetTeam({ userId, teamId }); const filter: any = { teamId, memberIds: userId }; const discussions: any[] = await this.find(filter).setOptions({ lean: true }); return { discussions }; }
Before calling find
, we call checkPermissionAndGetTeam
to check all necessary permissions. Discussion.checkPermissionAndGetTeam
is very similar to User.checkPermissionAndGetTeam
. Both are private methods - they are only accessible and used inside a corresponding Model. User.checkPermissionAndGetTeam
finds a Team document and checks if a user is indeed part of that Team, then returns the found Team object. Discussion.checkPermissionAndGetTeam
does the same things as User.checkPermissionAndGetTeam
but also checks if a user is a participant of a Discussion:
You've reached the end of the Chapter 8 preview. To continue reading, you will need to purchase the book.
We keep our book up to date with recent libraries and packages.