If we were in js land, we could have written this in signup.ts
consterror=newError('Invalid username or password');error.reasons=errors.array()//reasons a made-up property and errors is the validationResultthrowerror;
Encoding More Information In an Error
D 17-err:
D 19-issue:
D 18-err:
When custom properties needed to be added, then it's a sign to use Subclasses
js land code can be made as a subclass of Error
D 20-err: Entire big picture
Subclassing for Custom Errors
Create errors dir in src which holds different custom subclasses of Error
ValidationError is a type like the below error
[{msg: 'Bad email',param: 'email'}]
Here in request-validation-error.ts constructor argument is private because we want to take the errors property and assign it as a property to the overall class
//i.e. without private in constructorerrors: ValidationError[]constructor(errors: ValidationError[]){this.errors=errors;}//Equivalent toconstructor(privateerrors: ValidationError[]){}
List of ValidationErrors, so the ValidationError[]
thrownewRequestValidationError(errors);// errors is a list of errors
We will use public for errors for now, later we will switch back to private
We are hardcoding reason in database-connection-error.ts
Do we need to extends Error in database-connection-error.ts to make it a subclass
Here just an example for other type of Errors
Import both of these in signup.ts
Determining Error Type
In error-handler.ts, import the 2 subclasses of Error
Open Postman and send
{
"email": "test",
"password": "2343242332432"
}
//Response
{
"message": ""
}
// IN the terminalHandling this error as a RequestValidationError
Next test db connection error
{
"email": "[email protected]",
"password": "2343242332432"
}
//Response
{
"message": ""
}
// IN the terminalHandling this error as a DatabaseConnectionError
Converting Errors to Responses
D 20-err:
D 22-co: Common Response Structure
Send back an object that has an errors property
This errors in an array of objects
In request-validation-error.ts, Hover over ValidationError and see the different properties
Then back in error-handler.ts, build formattedErrors
D 23-err: Custom errors for every kind(Intricate knowledge on every kind of error is not feasible as the middleware's complexity increases)
D 22-c: To make sure they follow common structure
D 24-re: Solution to the above problem
Add serializeErrors method and statusCode to both the subclasses of Error
Check with Postman whether these refactor doesn't have any mistakes
Need to get the same response as above
Verifying our Custom Errors
There is nothing in our code to check whether serializeErrors is put together correctly
D 25:
D 26-ce:
Two possible approaches:
D 27: Option no 1
interfaceCustomError{statusCode: number;serializeErrors(): {message: string;field?: string;//? meaning Optional}[];// Return type: array of objects}// Then we can do something likeclassRequestValidationErrorextendsErrorimplementsCustomError
D 28: Option no 2
Abstract Class
Interfaces fall away in the world of JS, but Abstract classes translate to class definitions in JS
Final Error Related Code
Create custom-error.ts in errors dir
Create new Abstract class in there
abstract property to make it as compulsory property in subclass
abstract serializeErrors is a method signature
Import this abstract class in both the Subclasses of errors
thrownewError('something went wrong')// For logging behavior
So CustomError constructor takes an argument and because of that RequestValidationError and DatabaseConnectionError constructors also take an argument
The above is only for logging purposes and is not sent to the user
Now we can delete two if statements to just one if statement(Smart Refactor)
Make a quick test in Postman
How to Define New Custom Errors
Eg for route that does not exist
Go to errors dir and create not-found-error.ts
Just extend CustomError and hover over the errors to implement it(This approach is brilliant)
Import this in index.ts and before app.use(errorHandler), add app.get
Do a quick test in Postman
// Quick test to GET ticketing.dev/api/users/signup/adsfkdasf// Response
{
"errors": [
{
"message": "Not Found"
}
]
}
Also can change to app.all to handle all requests
// Quick test to POST ticketing.dev/api/users/signup/adsfkdasf// Response
{
"errors": [
{
"message": "Not Found"
}
]
}
Uh Oh... Async Error Handling
Adding async to app.all function breaks our App
app.all('*',async()=>{thrownewNotFoundError()})
Go to Postman and check send a POST request
// Quick test to POST ticketing.dev/api/users/signup/adsfkdasf// ResponseIt hangs Sending request...
We are creating this function for Typescript to get involved
But we need to import 2 different things(User and buildUser)
Adding Static properties to a Model
Add a new method to the User model
userSchema.statics.build=(attrs: UserAttrs)=>{returnnewUser(attrs);};// this is how we add a custom function built into a model//But when we try to use User.build() we still get this errorProperty'build'doesnotexistontype'Model<Document>'
TypeScript still doesn't understand it
To fix it we create UserModel interface
Defining Extra Document Properties
D 7-ts: Issue#2
To address Issue#2, create another interface called UserDoc
UserDoc and UserModel can be seen as arguments to model
They are types being provided to the function
Command click on model to understand better
UserModel is the return type of model
User Creation
D 3-auth:
In signup.ts, import UserModel
Status of 201, record was created
Open up Postman and test
ticketing.dev/api/users/signupHeaders: Application/jsonBody: Raw and json selected
{
"email": "aalkdsfjlad",
"password": "1"
}
// Response
{
"_id": "5efeff84d8ef4e00248d0c3f",
"email": "[email protected]",
"password": "2343242332432",
"__v": 0
}
// Send the same request once again// Now the response is
{}
Proper Error Handling
Throw proper error for existing user
BadRequestError for anything that goes wrong in our Request handler due to the input the User gave us
The above is different from RequestValidationError
RequestValidationError is for handling output from express-validator
Create bad-request-error.ts in errors dir
General rule of thumb for new errors is:
Import custom-error and typescript will give the details of what to do
this.message in super of BadRequestError is not possible becasue typescript jumps in and saves a reference to message on our instance
Go to Postman and do a quick test
{
"email": "[email protected]",
"password": "2343242332432"
}
// Send the above twice// Response after sending twice
{
"errors": [
{
"message": "Email in use"
}
]
}
Place Password Hashing logic in User model file, in other words in models user.ts
But we will place majority of the Hashing in a separate class which is in a separte file
We are doing this to make our user model file a little bit cleaner
Create services dir(Better name can also be given) in src dir
Create password.ts inside that
Create static methods in Password class
Methods that can be accessed without creating an instance of the class
Password.toHash('akldsajf')// instead of (newPassword).toHash('aldskfj')
crypto and util are built-in libraries
scrypt is the Hashing function we are going to use
scrypt is callback based implementation, so we are importing promisify to turn into a Promise based implementation which is compatible for async-await
Generate salt which is part of the Hashing process
Salt is random data that is used as an additional input to a one-way function that hashes data
When using scrypt, we get a buffer,i.e. an array with raw data
If we mouse over buf(which is little bit greyed out), it's type is set as unknown
constbuf=awaitscryptAsync(password,salt,64)// Add as Buffer(interface)constbuf=(awaitscryptAsync(password,salt,64))asBuffer;
Comparing Hashed Password
D 10-signin:
Mongoose Pre-Save Hooks
pre('save') is a middleware function implemented in mongoose
Mongoose doesn't have great support out of the box for async await syntax, so it has done argument to deal with asynchronous code
After the await call, at the very end we need to call done
Also note, we are using function and not arrow function
This is because:
Whenever we put together a middleware function, we get access to the document being saved(which is the User we are trying to persist to the Database) as this inside of this function
If an arrow function was used, then the value of this in the function would be overidden and would be the context of this entire file as opposed to the User document
Check for modified password, the reason being
We might be retrieving User out of the db and trying to save them back in the db(Situation: Email change functionality: Fetching the user, changing the email and save them back into the db)
Even we are creating the password for the very first time, it is considered as modified
Stored by browser and automatically sent to server while making follow-up requests
D 2-jwt: JWT
Arbitrary information: Payload
We can extract the original object anytime using a decoding algorithm
D 3-jwt: Where the JWT is placed
D 4-jwtcookie: Differences
Cookies any kind of data like Tracking info, visit counter
JWT need to managed manually unless we are storing JWT inside a cookie
Microservices Auth Requirements
D 5-ms: Requirement #1
Store info in auth mechanism to know more about the User,i.e. whether they have billing info, email etc
D 8-auth: Requirement #2
Admin User creating free Coupons
Need more info like their role(Auth info)
D 6-exp: Requirement #3
Tamper-resistant way to expire or invalidate itself
D 7-gr: Requirement #4
Understood by many different languages
D 9-summ: Requirement summary
D 10-sum: Steering to JWT
Cookie expiration handled by browser
But a User can very easily copy the Cookie info and just ignore the expiration date and continue using the cookie
D 3-jwt:
Can be sent in Request Headers Authorization
Can be sent in Request Body token
Can be sent in Request Headers Cookie
Issues with JWT's and Server Side Rendering
Normal flow: Loading process for Normal React application
D 12-auth: Where we care about authentication
D 13-ssr: Server side rendered application
Initial request to some backend server(client)
The backend server is gonna build the HTML for our entire app and send it back
So no follow-up requests required
Server side rendering
For SEO: Search engine optimization
Page load speed if a user has an older device or a mobile device
D 14-auth:
Very first request, JWT needs to be communicated
D 15-first: But this really presents a big issue
When you type google.com into your address bar in the Browser, Google has no ability to run js code on your computer(browser) before sending you an HTML file
When you enter after typing google.com, first thing you get back is the HTML file and inside that we can have some js code or a reference to a script tag to load up some code
And in that point of time, Google can start to reach around and try to find the tokens stored on your device
D 03-jwt: Only this is possible during Server side rendering(i.e. Sending info via only cookie and not through Request body or Header's authorization)
Check the examples in the documentation of cookie-session
req.session.views=(req.session.views||0)+1
We do a req.session
req.session is an object created by cookie-session middleware
Any information we store inside will be serialized and stored inside the cookie
npmjs.com and search for jsonwebtoken
payload is the info we want to store inside the jwt
verify method to check if user messed our jwt
cd auth
npm i jsonwebtoken @types/jsonwebtoken
IN signup.ts, import jwt
Right after we save the User to the database, there we want to generate the jwt
First argument to sign is the payload
Second argument is a private key, for now 'asdf', but later we are gonna change it
req.session.jwt=userJwt;// but typescript shows an error. To get around thisreq.session={jwt: userJwt}// This is because the type definition file that has been handed off to Typescript doesn't want us to assume that there is an object on req.session
The cookie-session then will serialize it and sends it back to the User's browser
Let's do a quick test in Postman
// change the email address to unique
{
"email": "[email protected]",
"password": "dasfdas238283"
}
// After sending it, go to Cookies tabNothing// It is because we didn't specify the HTTPS protocolMake request tohttps://ticketing.dev/api/users/signup/// Ingress-nginx serves an invalid temporary certificate// Disable SSL certificate verification// Now check in cookies tabexpress:sesseyJqd3QiOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKcFpDSTZJalZtTURjelkyTTFabVF5TVdSak1EQXhZVFpsTldZM01pSXNJbVZ0WVdsc0lqb2lkSFZ5YVc1blFIUmxjM1F1WTI5dElpd2lhV0YwSWpveE5UazBNekE1T0RJNWZRLkQ1ZEFtSkFKdTFTTjR5c21jSjM2dHVXOFA4OVBRZ1MtYWNFdU1iaWJ6N1UifQ%3D%3Dticketing.dev/Sessiontruetrue
So after this, anytime we make a follow-up request to anything at ticketing.dev, this cookie will be included and we are going to get our JWT
POST request tohttps://ticketing.dev/api/users/signin/
{
"email": "dsafdas",
"password": "dasfdas238283"
}
// Response
{
"errors": [
{
"message": "Email must be valid",
"field": "email"
}
]
}
{
"email": "[email protected]",
"password": ""
}
// Response
{
"errors": [
{
"message": "You must supply a password",
"field": "password"
}
]
}
Common Request Validation Middleware
Since there is duplicate code between signin.ts and signup.ts, we write a common validation middleware
Create validate-request.ts in middlewares folder
Import this in signup and signin.ts
Make a test on Postman again after making an invalid request
Sign In Logic
D 10-signin:
Provide as little information as possible if invalid credentials during signin(Due to malicious user)
Comparing the passwords is asynchronous
Copy paste Generate JWT from signup.ts
Sending back 200 status as we are not reading a new Record
Quick Sign In Test
Test in Postman
POST request tohttps://ticketing.dev/api/users/signup/
{
"email": "[email protected]",
"password": "password"
}
// Response
{
"email": "[email protected]",
"id": "5f0ac63a4093700033bda1aa"
}
POST request to https://ticketing.dev/api/users/signin/
{
"email": "[email protected]",
"password": "password"
}
// Response as well as Cookie
{
"email": "[email protected]",
"id": "5f0ac63a4093700033bda1aa"
}
// Test invalid cases as well like Empty passwords and Invalid password and an Email that doesn't exist
Current User Handler
Now fill up current-user.ts
D 11:
If user is logged in, there will be a cookie present
React application needs to know if user is signed in
React cannot directly look into the cookie if there is a valid JWT(We have set up our cookies like that, so that they cannot be accessed by JS running inside our browser)
So React needs to make a request to something in our App to know whether the User is currently logged in
The only time req.session will be null or undefined will be when we enter this Router handler(/api/users/currentuser) without first executing cookie session middleware given below
if(!req.session||!req.session.jwt)// can be replaced byi(!req.session?.jwt)
verify will throw an Error if jwt is messed around
Go to Postman and make a quick test
POST to https://ticketing.dev/api/users/signin/
{
"email": "[email protected]",
"password": "password"
}
// Response with cookie set
{
"email": "[email protected]",
"id": "5f0ac63a4093700033bda1aa"
}
// Then in new tabGET to https://ticketing.dev/api/users/currentuserWith headers:Content-Type: application/jsonPostman automatically sends Cookie when making request to the same domainSee Cookies on the right side: Of ticketing.dev// Response
{
"currentUser": {
"id": "5f0ac63a4093700033bda1aa",
"email": "[email protected]",
"iat": 1594543655
}
}
// After deleting the cookie and sending again// Response
{
"currentUser": null
}
Signing Out
Content in signout.ts
Signing out process involves sending back a Header which tells the Browser to empty out the Cookie, which will remove the jwt
To empty and destroy the cookie, we'll set req.session to null
Go back to Postman and make a quick test
First make sign in requestPOST request to https://ticketing.dev/api/users/signin/
{
"email": "[email protected]",
"password": "password"
}
// Make GET request to https://ticketing.dev/api/users/currentuser to confirm// Make another tab and make POST request to Make POST request to https://ticketing.dev/api/users/signout with Content-Type to application/json// Response
{}
// Make GET https://ticketing.dev/api/users/currentuser
Creating a Current User Middleware
D 12-mw:
Create current-user.ts in middleware
Augmenting Type Definitions
Making changes to existing Type definition
Go to Postman and make a quick test
Signin and check for currentUser
Requiring Auth for Route Access
D 12-mw: 2nd middleware
Create require-auth.ts in middleware
401 status for forbidden
Create not-authorized-error.ts Custom Error in errors
D 12-test: Require index.ts to test file to get access to the App variable(app is requiring express)
Difficult now as we have some hard-coded startup logic inside index.ts
If multiple services listen on the same port, we are gonna have some problem
But supertest assigns ephemeral port
D 13-split: Refactor for the above problem
Index to App Refactor
Create app.ts inside auth src dir
Cut everything above start function(except mongoose,cause you see a red wiggly line in index.ts when you cut everthing till start) and paste it to app.ts
Export app(Named export, cannot export app by itself) from app.ts and import it in index.ts
A Few Dependencies
cd auth
npm install --save-dev @types/jest @types/supertest jest ts-jest supertest mongodb-memory-server
In memory mongo because we can test multiple databases at the same time
Tests for different services concurrently on the same machine
This will run much quickly
We don't want this big mongodb-memory-server to be downloaded every time we build our Docker image
That's why we install them as development dependencies(--save-dev)
We are not gonna run tests in the image at any point in time
Update this info in the Dockerfile(--only-prod)
Test Environment Setup
Add test scripts in package.json of auth
watchAll: Run all different tests inside our project whenever any file changes
no-cache: Attempt to use Typescript with Jest
Without this, jest doesn't always recognize changes with Typescript files
Add another configuration for jest itself
ts-set for Typescript support
setupFilesAfterEnv: Tell jest to run setup files inside our project after it initially starts everything up
Try to run the file in the current working directory
Create test folder and in there setup.ts
New instance of MongoMemoryServer
Startup a copy of mongodb in memory
Allows up to run multiple different test suites at the same time across different projects without trying to reach out to the same copy of mongo
Also gives Direct access
beforeAll is a hook function
Runs before all of the tests get executed
beforeEach is another hook function
Runs before each of the tests get executed
Delete mongo collections Before each test starts
afterAll hook function after all tests are run
Our First Test
Test around signup Route handler
So, create a folder inside routes dir called test and in that create signup.test.ts
The above is the convention
supertest allows to fake a request to express app
it for test statement
We see async without await, because we will add it sometime in the future(Eventually when we make multiple requests in a single test)
cd auth
npm run test
We are getting an Error while trying to create JWT due to JWT_KEY environment
Previously, there was a check in index.ts but now it is split to index.ts and app.ts
Environment variable in deployment config file
Need to define env variable in test environment
We will do it in a simple, direct way(not the best) by doing it in beforeAll of setup.ts
Now check whether it passes
An Important Note
Sometimes jest or ts-test doesn't detect changes made to the file
Rerun npm run test
Testing Invalid Input
Try to sign up with invalid email or password
We could write assertions with appropriate error message, but we will just keep it really quick and simple based on status code of 400
Changes in signup.test.ts
npm run test takes a lot of time initially because we are setting up in-memory mongodb
But after that, tests run quickly
Press Enter to rerun
Instead of return we could use await
Requiring Unique Emails
Send same request again but with 400 status code
Changing Node Env During Tests
Session object(req.session which contains jwt) is turned into a string by cookie-session
Cookie-session will send it back to User's browser inside the Response
Cookie-session middleware will set a Header on the Response
Header name: Set-Cookie
sets a cookie after successful signup test case fails
supertest not making https connection as secure is made true in app.ts cookie-session
We could make secure to be false in test env
process.env.NODE_ENV != 'test'
Now we are gonna see that the test passes
Tests Around Sign In Functionality
Create signin.test.ts
Can copy paste some of the tests from signup.test.ts
But some small changes like password not required of some particular length
Testing Sign Out
Create signout.test.ts
Need to console log response to see what happens to the Set-Cookie Header
The above code is correct but we get the below error
Server Error
Error: connect ECONNREFUSED 127.0.0.1:80
This error happened while generating the page. Any console logs will be displayed in the terminal window.
But if we do only this and check the Response on Chrome Network tab, everything works properly
In the previous code snippet of LandingPage, it is executed on the Browser (also executed on the Server)
D 11-ssr:
When axios request is in LandingPage and getInitialProps is commented
First request, ingress-nginx sends to NextJS
Browser automatically adds the domain along with the Route
D 12-ssr: When it fails
Node networking on the client side
Node networking similar to browser and adds the domain
Since NextJS is inside a container, the request went there inside the container on port 80
But there is nothing running on port 80 inside the container
Two Possible Solutions
D 13-domain:
D 15-opt:
Option no 2: Not good as NextJS will need to know the exact service name for every thing it needs to reach out to
Also which route for which service
Option no 1: Which domain is the challenge
D 15-cookie: Including Cookie is a challenge
Cross Namespace Service Communication
D 15-opt: What should be the domain
D 16-ns:
But in my environment ingress-service is in Default namespace(That is ingress)
Ingress service is now in kube-system namespace
So it can be accessed directly
D 17-cross:
D 18-ext:
When is GetInitialProps Called?
D 13-domain:
D 19-ref:
Although we can technically can make requests inside the Component, we don't get the opportunity for the request to get resolved during the Server Side Rendering Process
Don't get the opportunity to update state, make use of any lifecycle methods
getInitialProps will also be executed on the Browser under very particular circumstances given below
Scenarios where getInitialProps is called
Hard reset and Reload: Server
Redirect from another page: Client
D 20-inc:
To test this out, changes in index.js of pages
Hard refresh on Landing Page
Log seen in server
Select Address bar and hit Enter
Log seen in server
Navigating from one page to another while in the app
Scenario: Signup
Not a full reload of the Page
ticketing.dev/auth/signup
Log seen in client(Browser)
On the Server or the Browser
window is an object that exists only inside the Browser
Specifying the Host
constresponse=awaitaxios.get('/api/users/currentuser');returnresponse.data;// toconst{ data }=awaitaxios.get('/api/users/currentuser');
In order to execute getInitialProps on the Browser, we have to navigate to this page(Signup process)
We see the data on the Browser console
If we are on the server, we need to make request to
name in the package.json should be @Organization/package-name
# Inside common
git init
git add .
git commit -m "initial commit"
npm publish --access public
# --access public is necessary, else it will think it is private inside our organization# Error, because we are not logged in
npm login
Project Setup
D 10-npm:
npm install typescript -g
cd common
tsc --init
npm install typescript del-cli --save-dev
mkdir src
code src/index.ts
We want to convert ts to js file
To do this, in package.json build in scripts section
Then in tsconfig.json, make the required changes
Uncomment declaration to have a type definition file
Uncomment outDir to have the converted code in ./build
npm run build
After build we get index.js as well as type definition file
Any time we make a change, we need to delete the build directory and build it again
So we add clean in scripts of package.json
Run npm run build to check
An Easy Publish Command
The main key in package.json indicates what file we import when we import the overall package
We need to make sure that we import the index.js inside the build dir
Also types key for the types file
And files key for what files to be included in the published versoin of our package
Make some change in the index.ts(like exporting color)
git add .
git commit -m "additional config"# The below command to automatically update the version in the package.json file
npm version patch
npm run build
npm publish
Read Semantic Versioning for how to version your packages
To make our lives easier, instead of running what is in that sh snippet every time, we'll write a little script for that in package.json under pub key
Make another change in index.ts and test npm run pub
Relocating Shared Code
errors and middlewares of auth are the reusable folders required
Drap them to src of common from auth
For example, if auth wanted to use BadRequestError
import{BadRequestError}from'@rzticketing/common/errors/bad-request-error'// But we would want it like the belowimport{BadRequestError}from'@rzticketing/common'
Option #1 would work without changing anything
But the downside is they need to know the directory structure of the class or function to be used
But for Option #2, we need to set this up in index.ts
export*from'./errors/bad-request-error';// Import everything and immediately re-export it
We will get errors if we try to build
Anyway, try buildigin it with tsc in common
This is because we don't have the modules installed in auth package.json
So we need to install via npm
npm i express express-validator cookie-session jsonwebtoken @types/express @types/cookie-session @types/jsonwebtoken
tsc again
npm run pub
Updating Import Statements
Correct Import statements in auth
cd auth
npm i @rztickets/common
Start skaffold back up to see if it's working
Updating the Common Module
Make the changes in index.ts of common or any other folder
npm run pub in common
Inside auth, npm update @rztickets/common
How to check if the container is running the correct version
kubectl get pods
kubectl exec -it auth-depl-789b7f647b-2ttbk sh
cd node_modules/@rztickets/common
cat package.json
TicketDoc in models is for adding any additional properties in the future
Defining the Ticket Model
Create ticket.ts for the model
Creation via Route Handler
Import Ticket model in new.test.ts
After getting all the tickets using find, we expect it to be 0, because before each test, cleanup is done
Save data in new.ts when request comes to /api/tickets
We get a red wiggly line in currentUser.id
But we are throwing an error if currentUser is not defined in requireAuth middleware
So, add an exclamation
Testing Show Routes
D 1-tik:
Create show.test.ts
For returns the ticket if the ticket is found test, 2 methods
Access ticket model directly using Ticket.build and save
Make request to build the ticket on the fly
Option #2 is liked by the author because it simultaes us using the API directly
Unexpected Failure
Create show.ts
Whenever we leave off the statusCode, the default is 200
What's that Error?
error-handler.ts is handling all our Errors
There you see status of 400 if it is not an instance of CustomError
So, in show.test.ts, console.log the response to check
Remember to remove the expect, else you won't see the console log
{errors: [{message: 'Something went wrong'}]}
If we add a console log in common module's error-handler.ts, we have to rebuild the common module again, set a new version, publish to npm and install the updated version of the common module in the tickets service
So hack for this is to go to tickets node_modules @rztickets/common/build/middlewares/error-handler.js
After checking the console log, don't forget to remove that
If there are 2 copies of a service, then assume posting a comment
It will be added to db 2 times because of the 2 copies
D 23-q:
If we want to send it to one of the copies, it can be done via Queue Groups
A Queue Group is associated with a Channel
Send it to only one of the members of the Queue Group
This can be done easily in subsribe's 2nd parameter
Now we see the event only in one of the 2 listeners
Manual Ack Mode
Other options to subscribe in listener.ts
It is the 3rd argument to subscribe
D 25:
Default behavior of event being lost when there is some error
To change this, we provide setManualAckMode(true)
This changes the behavior of node-nats-streaming library
node-nats-streaming library doesn't automatically acknowledge the event to the nats-streaming library
If no ack is received by the nats-streaming library after a certain amount of time(30s), it is going to send the event to the same service or another member of the Queue group
So until ack is received nats-streaming library will keep sending the events
To ack, we have to add msg.ack()
Client Health Checks
In nats-depl, we exposed 2 ports, 4222 and 8222, one for client and the other for monitoring
Just like previous 4222 port-forwarding, we need to do that to 8222 as well
Also we have a condition that amount should not be less than 0
Publisher has events
account: deposit $70
account: deposit $70
account: withdraw $100
IN ideal case scenario,
Publisher publishes events one by one
Since both the Account Srv are members of QueueGroup, only 1 of them receives the Event and update the Total amount of money for a customer in Hard drive file
D 27: Listener can fail to process the event
What can go wrong?
The file might be already locked(Some other program might have this file open)
Faulty logic like a weekly deposit limit, so in that scenario we might reject that event
So, when it fails to be processed, with our current setup, we don't acknowledge the event
So nats-streaming-server waits for 30s and sends it off to another listener
But while we are waiting for those 30s, another event gets published and it gets processed in the listener(Consider deposit 40 successful) and a couple of seconds later even withdraw event of 100 get published. But now if we try to withdraw 100$ out of 40$, we get a business error
So if any event fails to be processed, we will end up in a catastrophic situation
D 28: One listener might run more quickly than the other
If one service has a backlog of events, because this container might be overloaded or who knows what
As we are waiting for the events to be processed(deposit of 100 and 40 in one listener), account: withdraw event might be published and might get sent to the other listener which is very fast
Again we end up in the negatives
D 29: NATS might think a client is still alive when it is dead
NATS waits 30s for the ack, but the service is dead
So, within that timespan withdraw event might be sent, and we end up with Business error
D 30: We might receive the same event twice
1st deposit of 70 on tuesday
2nd deposit of 40 on wednesday
withdrawal of 100 on thursday
But consider the hard drive is very laggy
Consider it takes 29.99 seconds to open the file
NATS takes the same event and sends it to other service
But by this time, processing is done(110-100=10) and is written to the file
But since the same event is processed by another listener, we end up with Business error
D 1-sync: Concurrency occurs in Synchronous communication as well as in Monolith architecture
D 2-sync:
Monolith instance A and B are busy, while C processes quickly due to low traffic
Once again, we end up with Business error
But in microservices based architecture, the concurrency issue is more prominent because of latency of NATS-streaming server, retries etc
D 3-solution: This won't work
One copy of Accounts Srv instead of 2
Same issue as before when 70$ event fails to get processed
But now we have processing bottleneck
D 4-solution: This won't work either
More Possible Concurrency Solutions
D 6-state: Share state between services of last event processed
Account Srv B processes event no. 2 only if event 1's sequence is recorded in the Shared Processed Sequence
But this has a significant impact on the performance
Consider if there are 2 Users A and B and 1st event if for A and the 2nd event is for B
Now if the processing of event A is delayed due to some issue, B still needs to wait, even though it is unrelated to A
D 7-state: Taking the learnings from the previous process
We consider sequence numbers w.r.t users(Added by NATS Streaming server, not known by Publisher)
In this way, one User doesn't affect another user
But with NATS-streaming server, we need to come up with a separate channel for each resource(in this case, Users)
Also a lot of processing overhead
D 8-state:
Store the list of events at the publisher
Publisher has some kind of db
NATS Streaming server sends(Hopefully) a sequence number to the Publisher for the event just dispatched by the Publisher
Then the event will go on to our services and be processed
The Last ID processed is updated by the Account service in the db
The next time Publisher sends an event for a particular user, it will attach the Last Seq. No
When the Account service is processing, it will look at the db, whether the Last ID processed and Last Sequence No in the event are equal. If it's that, then it will process and update the db
But the problem is we can't reach out to NATS Streaming Server and get the Sequence number of the event
If number: 1 transaction gets delayed for some reason and if number: 2 is getting processed, then we check whether number - 1 is there as Last Txn number in the db
Here 2-1 is 1, which is not there in db
So, we will not process that event until 1 is processed
Concurrency Control with the Tickets App
D 13-req:
D 9-refr:
Orders Service will be having some logic to process the event only if the version in the Event is 1 less than that in the db
Event Delivery
Stop one of the listeners
D 20-re:
Events saved in NATS Streaming when it is emitted by Publisher
We can get events delivered in the past
But this is somewhat different in queue-groups, so we will disable that for now
Another option to the list of options to redeliver
Command click on SubscriptionOptions to see the interface
Use setDeliverAllAvailable
But this is not super feasible as if there are millions of events and every time a new listener is added
We get all the events from the start
Durable Subscriptions
D 21:
Identifier to a subscription
setDurableName()
So with Durable Subscription, we have events processed against the Durable Subscription in NATS
setDeliverAllAvailable is a necessity as it gonna be called for the very first time only when used with Durable Subscription
So, when we restart the Listener or bring another Subscription online, then setDeliverAllAvailable will be ignored on restart
But when we test this on our listener, we get all the events redelivered
So there is a little bit of gotcha
When we restart we are closing conection to NATS and reconnecting, NATS thinks that the client disconnected and won't be connecting in the future
So it will clear the Durable sub history
To solve this, we need to add queue-group
So, setDeliverAllAvailable, setDurableName and queue-group are very tightly coupled options
What we want to achieve is to somehow get the properties of data in listener.ts
Also, it would be good if the data is tied to the subject(i.e, if ticket:created is the subject, then data should have so and so properties of type so and so)
In order to achieve this in TypeScript, we need to write some complicate code
Subjects Enum
Create a separate file to export an enum of Subjects
Create ticket-created-event.ts which contains the interface containing the coupling details of the subject and the data
Just like this, we create separate interfaces for other events
Enforcing Listener Subjects
Make Listener a Generic Type class to tie subject and data
Quick Note
Tyepescript does have a keyword of readonly
It prevents a property of a class from being changed
Enforcing Data Types
Enforcing using the TicketCreatedEvent['data']
Where Does this Get Used?
D 10-common: Subjects, Base Listener and interface describing the data for the subject in common module
Custom Publisher
Goal is TypeScript to check our code
Create base-publisher.ts and ticket-created-publisher.ts
Using the Custom Publisher
Using it in publisher.ts
Awaiting Event Publication
publish is an async operation
To do this, we need to return a Promise from base-publisher.ts
Common Events Definitions Summary
D 14-test:
D 11-common:
D 12-ts: Downside all servers are written with Typescript
D 13-ts: Alternatives to TS
Updating the Common Module
Create events in common folder
Add userId to ticket created and updated events as there is userId in tickets model file
Don't forget to export in index.ts so that it can be easily imported in other files
cd common
npm i node-nats-streaming
npm run pub
cd tickets
npm update @rztickets/common
Restarting NATS
Flush out non-valid events
kubectl delete pod nats-depl-58c5f75f5c-pngb2
Publishing Ticket Creation
Create events folder
More on Publishing
Right after ticket save call, publish the event
Pulling title and price from title should be done because it might be different values due to pre and post save hook operations done while saving to db
If you are not signed in then make to POST request to https://ticketing.dev/api/users/signin with the email and password in body(valid credentials) or signup
D 11-async: Behind the scenes in Ticket Create Handler
D 12-err: Error in the above scenario
D 13-update: Behind the scenes in Ticket Update Handler
User doesn't need to know about the Events
Adding await to Publisher adds a tiny bit of latency
D 4-note: What would happen if emitting event fails(Impact on the App)
D 5-nats: Continued from above
Users account balance never gets updated
Question about Data Integrity
Failed event causes Data Integrity issue(Accounts not updated due to the event failing)
Handling Publish Failures
D 5-nats:
D 6-tx: Straight approach to the above problem
Save the event to our db
D 7-proc: Separate process outside our Route handler
Any issue with db, we don't save that transaction and the event and there is no data integrity issue
If NATS is down or connection to nats is down due to some crazy reason, we can still save the transaction to the db, save the event and then later when NATS comes up, the separate process will pull the event and publish it to NATS
Another scenario, D 7-fail: Fail to save the event
Due to db constraint or something like that
If failing of either transaction or event fails, then we have to undo(like rollback the transaction or vice-versa)
D 8-tx:
Database feature called Transaction.
If any of the changes fail, do not make any of the changes
So we wrap up both transaction and event in a DB Transaction
Not goint to implement in this Application
Fixing a Few Tests
cd tickets
npm run test
D 14-tests: Normal env while running our app
D 15-client: Test env
D 16-mock: Jest redirect to Fake initialized NATS Client
Redirecting Imports
D 17-mock: Mocking(Faking) Imports with Jest
Create mocks and in there nats-wrapper.ts
jest.mock of the file which we want to fake
Close npm run test and run it again
We made the change only in new.test.ts
So to only run that file, type p and then put in new
Now we get a different error message, which indicates that our fake file is being used
Providing a Mock Implementation
D 17-mock: Mocking(Faking) Imports with Jest
D 18-mocks:
New Ticket Route Handler doesn't care about _client and connect function
Take a look at how TicketCreatedPublisher uses nats client and implement same functionality in mocks file
TicketCreatedPublisher doesn't do anything
So need to take a look at base-publisher.ts in common/src/events
Real client calls the callback function to indicate publish is complete
D 19-mocks:
D 20-pub:
We want to make sure our callback function gets executed right away so that our Promise will get resolved
Now we see that all of our new test is passing
Test-Suite Wide Mocks
Copy jest.mock to all the other test files which need nats client
Instead of doing the above, we can use only in setup.ts
All of my tests pass
Ensuring Mock Invocations
Provide a mock function which allows to have expectations around it
Here in new.test.ts, even though we import the real nats-wrapper, jest will use the mock
Also clearAllMocks since we are using the same mock function in other tests too
NATS Env Variables
Add env in ticket depl
After all these changes, check whether ticket srv is getting connected to NATS in skaffold logs
[expiration-depl-675f94cf9-9mwr7 expiration] Message received: order:created / expiration-service
[expiration-depl-675f94cf9-9mwr7 expiration] I want to publish an expiration:complete event for orderId 6003115eae86e60018be7b1e
Delaying Job Processing
Create new ticket and order
See in skaffold output
Defining the Expiration Completion Event
Create expiration-complete-event.ts in common module
cd common
npm run pub
cd expiration
npm update @rztickets/common
Publishing an Event on Job Processing
D 10-queue:
To test this quickly, go to order-created-listener in expiration
Comment delay
Test using req.http Ticket and Order creation
[expiration-depl-5f6d5d6586-qrktq expiration] Message received: order:created / expiration-service
[orders-depl-5d776b76fd-ljdv7 orders] Event published to subject order:created
[tickets-depl-9767f78d-89g46 tickets] Event published to subject ticket:updated
[expiration-depl-5f6d5d6586-qrktq expiration] Waiting this many milliseconds to process the job: 59982
[orders-depl-5d776b76fd-ljdv7 orders] Message received: ticket:updated / orders-service
[expiration-depl-5f6d5d6586-qrktq expiration] Event published to subject expiration:complete
Also no need to buildClient again in the components
Pass the already built client to the Components in _app
Scaffolding a Form
new.js form
Sanitizing Price Input
state for keeping track of title and price
blur: click in and out
Backend for validation
Ticket Creation
getInitialProps: to show the component on the screen(Render for the first time)
use-request hook
Listing All Tickets
Initial rendering of all tickets in getInitialProps
return of tickets from getInitialProps passed as tickets props in LandingPage
Linking to WildCard Routes
D 2-routes:
Manually test via ticketing.dev/tickets/asdkfjlajdfs
Both href and as
as is the real route
Creating an Order
context contains the id
Programmatic Navigation to Wildcard Routes
D 1-mocks:
Router.push: Programmatic Navigation
1st argument is path to file
2nd argument is actual url
The Expiration Timer
Whenever we return a function from useEffect, that function will be invoked when we are gonna navigate away from the Component or rerendered(dependency in the array for rerendered)
Displaying the Expiration
Condition after useEffect
Showing a Stripe Payment Form
react-stripe-checkout library in npmjs.com
Configuring Stripe
Need to use stripeKey as env variable in next
Test Credit Card Numbers
stripe.com/docs/testing
Paying for an Order
For doRequest to merge token along with previous body, change in useRequest.js
In ticketId.js, event gets passed automatically as first argument
So change it to onClick={() => doRequest()}
Filtering Reserved Tickets
In tickets index.ts routes, find only tickets where orderId is undefined