Lesson 14
Implement Message Creation for ExpressJS Backend - Bootcamp Day 14 (2024-09-23)
Source Code: https://github.com/nbktechworld/full-stack-web-dev-3/tree/b87f9e77683870c7f915c641811ffbe2938e7f58
The lecture goes over implementing the backend JSON API endpoint to create a message for the message board.
You learn how to filter and validate user input on the server side.
The lecture goes over ExpressJS middleware. How to install the cors and json middleware.
You also learn how to make HTTP requests with Postman.
The lecture also goes over debugging Node using the Visual Studio Code debugger.
Finally the lecture goes over making a SQL database query using the pg package so a new record is inserted into the messages table.
Video Transcript
Let's continue building the application programming interface API.
So far we have the ability to list messages, however we do not have the ability to create
one.
This is what we got so far in the browser, so we can list all the messages and all of
these actually come from a database.
You recall we're using Postgres.
Here's my graphical user interface, GUI, Postgres, PGAdmin, and I got the database here, right
click the table messages, view added data all, and here are all the messages, so that's
what we're seeing right now.
The data that's in the database is now present in the front end.
If you recall we have two terminal tabs, one for the back end and one for the front end,
and they're running at the same time.
Okay, let's go to server index, and I can close this, but it's going to be running in
the background, so like hidden.
And because we have node mon decode for the server when we make the changes, we'll automatically
restart the server.
So we have the function set up app where we define the end points here, let's define
another one.
So what we do is we're going to follow the same approach.
So outside here we're going to add app.
We're going to add app.post because when you want to create a resource and you follow
the rest convention, you use post to create one, and then the end point will be slash
the resource name.
So we're going to call this function post.
First argument is the path, the name of the resources messages in the plural, so I just
say slash messages.
Second is the function that will handle the request, and it has two parameters, request
and response objects.
And then here we have to read what the user sent through the request, buddy, filter validate,
user input, call on the database to create the record, send back a response with the
created record, including the ID.
So typically what you would do for the handler is you have to extract what the user sent
from the request.
Usually it's in the request body, right, the user from the client side makes a post request
and includes that information in the body of the request.
Then it's your role to read that, extract, and then what you're going to do is you're
going to first filter out on desired properties.
You just want to take what you need because there might be some users who are not in good
intention, so they're including extra information that might not be relevant to the resource
operation at hand, right, if you're trying to create a message and somebody tries to
not only send a message text, but something else completely unrelated in hopes that that
somehow is going to hack the system, so that's why you want to only isolate what you need.
In this case, you want to filter out all properties except the message text.
And another thing is validations, for example, has the user provided any message at all?
How many characters do you expect at least to have for a message?
One character, what about at most, like for example, Twitter or X, it used to limit the
characters, right?
I think right now it's around 300 or something, so the user cannot type more than that.
And all those other validations as needed.
And now after you pass the validation, you would call the database to create the record
and finally you get an ID that identifies the row and then you send that back to the
user.
Usually you send the full object including the message again back to the user.
It's like echoing but with the ID included.
Something I didn't mention is in an actual system, you're going to have authentication
and authorization, meaning when a message from the client comes in a request, you're
going to first check, okay, is this user authenticated into the system?
That is, do you have an account of the system?
Because usually we need to require somebody to register before actually creating a message.
And the other part is, are you authorized to do that operation?
Maybe you're trying to comment on something, a protected resource and that is not allowed.
So you have to check those.
Usually those two are handled using what we call middleware, which is something that's
put in between the request and the actual response handler.
So by the time I reach this handler here, I will have checked if the user is authenticated
and if he user is authorized to do this operation.
So we can assume that, okay, since we don't have it, we're going to make the assumption
that is the case.
Anyway, how can we read the body?
Well, we got to take it from the request body and that might involve some stuff.
So usually we use middleware as well to extract that for us automatically for every request.
And there's already a built-in into Express called the JSON middleware.
So what we're going to do here is we're going to go before we do all the handlers right
after we connect to the database and we're going to add the middleware.
So to use a middleware, you say app.use and you pass a function here.
Now I'm going to explain to you in simple terms what a middleware is.
It's just a function that takes a request, response and a next.
Now the middleware is going to be intercepting the request that come in.
So when you get a request, it's going to call this function here.
And then you're going to do whatever you need to do.
In this case, it's going to extract the body, but I'm just explaining to you what the middleware
is.
So after you do your operation, do some processing.
You have to let the request go through if that is the case.
So if you want it to go through, you call next like that, okay?
So this is how you'd build your own middleware.
If you need to check for authentication, here is before the next, you would check, okay?
Let's see if the user exists in our database, is he authenticated and so on and so on.
If that is the case, I'm going to call next and let him through.
Otherwise, I just block send back a request, return ras.send with some error or whatever.
Okay?
So that's a middleware.
Now we don't have to write from scratch the middleware to extract the body of the request.
We can just call the express.json middleware like this, okay?
So when you call this one, it returns back a function to you.
And that functions definition for the middleware that will be in charge of every time a request
comes in, it will extract from the body and turn that into a JavaScript object, JavaScript
object notation.
You can actually see the source code for this thing here.
If you go here and look at, this is the type actually, let me look at the node modules,
the rectory, express, live and this one, I think we can search here for JSON and it can
read this line, express that call to JSON is actually from body parser, this thing here.
And that comes from this other module.
So we would have to go to node module slash body parser and index.
And this is actually where that JSON is defined.
You would find JSON here somewhere.
And I think it's this one.
Anyway, you can go on and on finding out what the code looks like.
I just wanted to show you that's just returning a function.
Think this one, if you export JSON, anyway, going back to where we were.
So this will return that middleware function.
So every time you get a request, it will extract.
So where is it extracted to?
It's extracted to rec.body.
So it sets the property rec.req.body.
So when you go back to the handler for the post messages, we can just say rec.body.
.whatever property was specified in the form.
Remember the form, the controls for the input, they have a name property.
So this is what will appear here.
So if you had the name set to message, you would just call message.
Anyway, so if you recall, let's go back to the front end to see what we are sending
if we're sending anything at all.
Components messages.
So when we add a message here, so we're calling the fake back end
and we're sending the body with the title like this.
OK, so we want to change this.
So first, we're not going to call the fake back end.
We're going to delete that.
We're going to call our own that we just wrote with express.
So HTTP, no s, OK, no s, colon slash slash localhost colon 2001 slash messages.
Now this is in the front end and it's already including these options
to make it a post request and it's including the title here.
So when we go back to the back end, we're going to read rack.body.title.
OK, so let me save that.
Go back to the back end server slash index and to access the title is just this.
OK, now to filter out properties, typically we would if we had more than one.
So right now it's very simple.
So maybe you could do like make a function that will call on the body to filter.
Like for example, I can make a function here.
Let me put it outside.
Function filter properties of a request body.
Then you would do the filtering and here we can do a very simple one.
Just return the object with title is request body.title.
If you had more, you have to write it down or you can make like a for loop
to kind of eliminate or filter out what you don't need.
Anyway, with that function, you can call that here.
So filter properties with the request body.
So basically you give the request body, which will be that object of the title property.
And then the filter properties functions as in charge of filtering out any undesired properties.
And that's doing so by simply returning a new object
with the title property extracted from the body.
So this way if you had like extra properties in rack.body,
they would be taken out and you just get back this filter one.
So you can call that let's call it message body, storing a variable.
And yeah, so this we did both at the same time.
Now we're going to call the database to create the record.
But actually we could also validate, right?
You want to validate the title.
Let's do this one here separately.
Maybe you want to check, okay, the message body.title, do we have a title?
So if we don't have a title, that is maybe this is not defined.
So you can use the exclamation there, which means this value here,
if it's like not defined, like undefined or no,
it will flip to true because of the exclamation mark.
That's the not operator.
And the body of the if will execute.
And here we would send back a response, for example, return res.
And then if you want to change the status code, status maybe four to two.
And then you would send an object of an error property,
maybe title is missing, something like that.
So dot status will set the response status code to four to two.
I think that's unprocessable entity.
And then if you send like this, it's basically the same as using JSON.
You want to use that as well.
And observe I put return here because I don't want any code after the if block
to be executed, so that's important.
So this will take care of the title not being provided or being empty.
If you want to add more validations you add, you would follow the same principle here.
Maybe you want to also check if there's a limit, maybe if the message body.title.
In this case, because this one passed, and I'm here in line 44,
I assume message body.title already is defined and it's non zero length.
So I can do, I'm safe to do message the title length.
For example, if I don't want it fits greater than 300 characters,
then I'm going to send this to, we have a limit, right?
I'm going to send here JSON error, title should not be more than 300 characters.
Something like that.
And you can keep going on and on and on.
And this actually can be extracted into its own function.
In the same way we had the function to filter the properties, you could go here,
maybe you can make a function to validate properties, something like that.
Body, and then you would do your stuff here.
Maybe you want to return back an error, check if it valid.
Maybe you could do like, you can make an object of errors or an array, it depends.
If you do array, you would basically do, for example, if the body.title, you would return.
It's up to you if you want to return, there are different ways to approach error.
You could accumulate them all, or you can just return the very first one that you see back
to the user so he can fix that and then he gets the next error and so on.
Or you could send them all at once.
If you do one by one, you don't even need the array of errors here.
You would just return like the message here, for example, title is missing.
You could do that.
And then you don't need this.
I'm just commented out.
You can also accumulate all errors instead of returning one by one or one at a time.
And then you would add the other one.
Let me do that.
You can call body.
You have to change it because I changed the name here.
Ready could change the message, but if you don't want to change this one, we just return that.
So this way, if you don't have an error, you could return like either empty string or null
or undefined.
So that way, to use that, you would call here so error validation errors equals validate
properties of the message body.
So you call the function and then you're going to check if there is validation errors.
That means that is defined.
You are going to do return res.
So you're going to call the function.
You are going to do return res status with the error being the actual validation error.
Actually, this should be singular, right?
Because I only have one.
And then you can do that.
Isolate into the function here.
So if there's no error at all, it's going to return no.
And when you return no, this will be a false value which turns to false.
And the body of the if statement is not executed.
Therefore, it's going to continue to the next lines.
Okay, so after that, we call on the database to create the record.
But before we do that, let's just test everything so far to see if it's working.
Okay, I'm going to just return a res.send here with okay 200.
Just so we can test it out.
So let's go here in the browser.
So you can press F12, open the network tab.
Let's monitor write message.
So we got a problem here.
And that's the cross origin request blocked because the domain the client
doesn't match the same as the server and the browser by default is same origin policy.
It doesn't allow that.
So it's that same story.
We have to add the header access control allow origin.
So you would have to go here.
And for this one, basically set the same thing you did line 44 here.
Basically that.
So before I send the request, I do that.
Going back here and trying again.
Let's see if I saved.
One moment.
I think there's a cache.
Maybe there's a cache.
Let me see.
See the request headers, response headers not being included here.
But if I do refresh and see the messages, I see it here.
So why is it not including it?
Let's see.
We changed the front end to go to our back end fake, right?
So I valid verify that.
It's actually doing a request to localhost 3001.
Let me see what's going on in the back end.
Let me restart.
But it shouldn't be the problem.
I saved it, right?
Okay.
Let's see.
It's not including here.
Oh, because of the validation, this is not being reached.
I think that's the issue.
If we move this back up, let's see.
No, let's see what's going on.
Okay, let's do some logging here to see what's going on.
A easy way to debug is just console log.
Pull slash messages here.
And we should see this message
when I go back and try again.
So it's not calling that.
So why is that not calling that?
This should have been logged.
Okay, something is strange going on here.
Let me remove the middleware.
Refresh, right, we're making posts.
And it's not logging.
Did we call in messages?
Let's see.
We call the messages HTTP.
Okay, let's
let me open false man here.
So there's this program that we can use to debug
make HTTP requests.
And it's easier to work with.
Call one of them is postman, but you can also use insomnia.
This one as well.
So it's up to you.
I like postman.
Anyway, I'm going to open my postman here.
One moment.
So you can you don't have to sign in for postman.
You can click skip there if you install for the first time.
And then click I'm going to plus the plus and you can
change my request here to post HP colon slash slash
localhost three thousand one slash messages.
And I can see now I see what's going on.
Do not read properties of undefined
that filter properties line.
You can see here line 16 of index JS.
So let's go back.
Now it's showing host messages.
I don't know why in a client it didn't make the request
because of course, even though I put access control,
it's probably because of this exception.
Didn't make it possible to set the header.
Anyway, let's see line 16.
It's saying reading title of undefined.
So this is undefined.
So who has calling filter properties?
It's calling with undefined here.
So it looks like my recta body is not being
parsed.
Why is that the case?
Let's check the middle where I think I comment it out.
So I should uncomment that and then try again.
Now we're going to click send.
Now I should get this title is missing,
which is correct because I didn't include anything.
Now the front end doing that is very strange
because I can see here headers and it's here.
Access control allow origin.
And if I go back to the front end,
something strange is happening.
Let me try Chrome.
Open network.
I disable cache by the way.
I click this.
I'm going to refresh.
Build to fetch.
Okay.
From origin have been blocked.
Let me go here.
Response headers.
I don't know why this is not including allow any postman it is,
but let me just kind of fast track
and use the cores middleware instead of setting it.
Myself there's also middleware for cores that you can put
so it can does everything for us automatically.
So we don't have to be doing it ourselves here.
So I'm going to remove this
and remove also from this one here.
And I'm going to go here.
I'm going to app.use and it's called cores.
You can call it like this and you have to install it.
So let me kill the server.
Control C and PM install C O R S.
So when you call cores like this,
so basically it returns a function
with that signature of rec res the next.
So I can go up here.
Cores require cores.
And that should work off the bat.
And I think it's going to be by default allowing everything.
If I am not mistaken, let's see.
Let me restart my server npm round dev.
Then I'm going to go to postman and test it out.
And try again.
I'm going to try again.
So I see star there.
It allows any origin.
So we're going to go to Chrome.
Refresh.
I don't really need to refresh, I think,
because it's going to back in change.
Now it's doing its job.
I have some break points here.
I'll remove all and click play.
So if you click back to network,
now it's making the request here.
You can see the response status include access control allow origin.
Okay, now it's working.
Anyway, let's go back to
yes code.
You can see the console logs are now being executed.
And there's no problem in the response.
So if once you've done console log,
you can remove it.
Okay, let's test these validations.
So we got the filter properties here.
You can test that.
Let me show you a way you can debug this without the console log.
So you're going to kill the server.
You're going to run with node dash,
that's inspect dash VRK space and then index.
So you need to call node with this option.
So you need to call node with this option.
And it will pretty much wait for you.
Then I'm going to VS code.
This is a debugger for visual studio code, by the way.
You can put break points here, for example.
I'm interested in seeing what message body is after doing the filter property.
So I can click here, the line afterwards.
See that break point.
And I'm just going to let it through.
So what I have to do is attach to the node process.
So I would go to view command palette or control shift P and attach to node process.
And then I choose the one that's running and it's make sure it's this one.
Make sure it's this one.
And now it's pretty much server running there.
So you have to make a request.
So we go to postman.
I'm going to test the request without anything, right?
So if I do that, I go back to visual studio code.
I'm now stopped in the break point line 55.
And I can hover over message body to see there is no title.
It's undefined, right?
So it's returning an object with title on the fine.
And then if I go to the next line, which is F10 or pressing this icon here,
it will also get the validation error title is missing from this function call.
So yeah, you can just let it play and it will return back a response.
Okay, and then I can try again, going back to postman.
I will go to body if I want to include anything.
And I can let me scroll all the way up here.
There's no room here.
Raw, JSON, make a JSON object, put a tire there.
Let's say hello world, and then I'm going to add other properties.
This isn't relevant.
One, two, three, another, I need quotes because JSON.
Hello.
So these have nothing to do with creating the message.
So I'm going to send.
Then I'm going to go back to the Visual Studio Code.
And I can see it's paused in the breakpoint.
Let's look at rack.body.
This is rack.body.
If you hover over it, you can also go to debug console,
rack.body here, enter, it will return to you that.
So as you can see, this is what we received in the server side.
So if you look at message.body, it filtered out the other properties and only leaves the title
hello world there, which is what we want.
Now we're safe to only leverage that when we insert it to the database.
So if I go to the next line with F10, there's no validation error, right?
No.
And it can continue.
You can also test the title length.
If you want to do that, you can do that as well.
I'm going to kill the debugger by disconnecting and going back to the term and control C
and just let it run in PM Run Dev again like normal.
And we can test the other one easily by going here.
I can copy this many times 300, right?
So let's do that.
We can do this 300 times.
So you can go to something like a, there's a, you can write, for example, Python.
You can type character A times 300 and it'll print 300 times.
Anyway, let's go here and change it.
That might be enough.
422.
And the body is better should not be more than 300 characters.
Okay.
So that's testing and debugging.
Now let's get back to actually doing the database stuff.
So we got everything filtered out, validated.
Now it's time to call on the database.
So we're going to hide this for now and just do the same process we did in the get.
You're going to call on the PG client.
So, hey, PG client, let me dot query and the query, if you recall from sequence inserting
to table name messages, and then we have to specify all the column names.
Here, and then the values here, let's refresh your memory about the table.
Going back to PG admin.
Let's expand messages and you can see expand columns.
We have ID body user ID created at updated ads.
Okay, let's go here.
So we've got body user ID, read it out, update it out.
So those are the columns, we don't need to specify ID because that's auto incremented
and we're taking care of now when you do the values here, you would put them here, right?
ABC and so on, but we don't do this when we write code because this would
expose us to what's called SQL injection problem security, in which if the user provided a title
that had a SQL query, for example, if you were to write it here, the user provide like title,
literally they would say like this, let's say they would put some fake value like,
they would put some fake value like, hello, then comma,
the user ID and then some date and then another date and then they would close it
and they would do something malicious like delete from messages.
So if the title is this, right, and you take that copy right this into here,
you would close the insert and then delete or do something malicious, which is undesirable.
This is very dangerous stuff, so it's called a SQL injection.
So to avoid that, you don't want to do that.
You want to actually do placeholders with question marks here.
And then as a second argument to query, you pass an array of all those values in order.
So the first one is the body, which will be message body.title, right?
We call body the title, it's kind of weird, but we could change it later, but it's okay.
That's what we got.
And the second one is the date.
I think we can do a new date like this.
Let's try this is basically creating a new date object and you can copy the same for updated ads.
And I put user ID actually before the date.
We don't have a user right now.
So we can do like, let's do current user dot ID and create a fake user.
We don't have off right now, but let's create a fake user.
So you could create a variable called current user.
That's an object that has an ID, for example, one.
That's like the fake user of ID one.
And then you can use it here.
And this query returns a promise.
Okay, so you would have to use the dot then like we did before.
However, I want to show you a different way of handling that with async await.
So instead of doing that, I'm going to do await that.
And then this await what it does, it will not go to the next line
until this promise is fulfilled.
And then we'll take the value and return here.
And we can return that put in a variable called the result.
Now to use await, I need to make the function that wraps that statement async.
So I would go here and add async keyword.
Okay, the difference between this and the promise is when you have promises,
handle with then everything outside here, here and there are actually run.
So this is called.
And at the same time, it keeps going to here and there.
And the then will only be called once this operation for this specific one is done,
but still the other lines are run.
So that's the difference here.
This one, this line is not run until this one is the promise is fulfilled.
Okay, we got the results there.
So typically the square, it doesn't return back the object.
Basically the row that was created, including the ID.
So you want to add to the end this returning star.
So basically returning all the the column names.
That way the results will be a list of rows.
So we can do a few access result rows.
And the first one will be the created one.
And you can send that back to the user here.
So you would send that.
But mind you, this result, the database tables, the naming might be not not be the
same as the one you're expecting because usually database columns in your case,
they're named with the underscore case, right?
If you recall, but usually in JavaScript, we use camel case.
So if you want, you could convert it here before sending it.
You could make like an adaptive function
to convert from snake to camel case.
Or you could also use SQL alias
as in your query.
That's also possible.
But anyway, you can make like a created message equals,
you would say title, and then you would say result rows sub zero dot body, right?
Because we don't call it title.
We call it body.
So this is we lick the adapter.
Because if the front end is expecting title, you have to send that back.
And then you send back create a message.
This also can be isolated to a separate function if you want.
I just did it in line.
So a lot of the things here, we're doing kind of a manual,
but you can always better organize it, make it, you know, less repetitive in a way.
Okay, let's try it out.
Finally, we're going to move this here.
Let's write pulse band first, because it's easy one.
So I'm going to make this message.
Hello, world.
One, two, three, send it.
And then what happened?
Oh, we got an issue.
Let's see what it is.
Open the terminal again, we crashed.
Oh, I deleted the whole query.
How did I do that?
Let me backtrack here.
Now we'll redo everything.
I did not mean to remove the whole query.
Now I'm going to make it all over again.
And this far I want to copy it back.
Okay, I didn't want, I didn't mean for that to be removed.
Okay, let's try again.
It restarted the server.
And we're going to go to postman.
Try again.
More errors.
Let's try again.
What's the other one?
Syntax error index 66.
So I made a syntax error.
So we got the query, inserting two messages, values, returning, got four question marks.
Got that.
Let's see.
Let's try maybe
if I had ID
and put the fault here.
Would it work?
I don't think that, that shouldn't be a problem.
I don't think I need semicolon, but let's try in this case.
Okay, same problem.
Okay, so we've got to do, how we get around this, I don't know what's going on.
So I'm going to backtrack.
So I'm going to comment this out and ignore this.
I'm going to comment this out and ignore it.
I'm going to comment the PG query ignore.
So I'm going to send back a reply here, return res.send.
Okay, and that should work.
Okay, I got 200.
Okay, nice.
Now going back, let's move this down here.
So I'm going to make the query.
So that seems to be inserting two messages.
Anybody, whatever, whatever, whatever.
Let's just do a query of a select, select star from messages.
Does that work?
And then I'll just remove this temporarily, but I'll revert that back.
Okay, that seems to work.
So I'm going back, revert.
So we got the ID, you got body, read it out, update it at, the column names are correct, right?
If I read PG, I mean again.
ID body, user underscore ID, read it out, update it at.
Yes, and messages is the table name.
We got message body here.
We got current user, new date, new date.
And that's the crash.
Okay, inserting two messages.
Let me remove returning.
Okay, not that.
Let me remove this whole thing.
It wouldn't happen if I do this.
Okay.
Okay, still not.
Okay.
Okay, let's debug with the dev tools again.
Control X, node, inspect, drk, index.js.
Attach to node process.
Make the request.
Go back here.
Okay, let's see message body.
Yeah, I have a title, current user ID.
I can do new date, right?
If I go here and debug console, that works.
And, yeah, insert in to messages ID, got body user underscore ID, created at, updated at.
Oh, I'm missing the, I'm not.
Thought I was missing the closing parentheses, values.
I got default, question mark.
Oh, okay.
You got to use dollar one here, dollar two, dollar three and dollar four.
It's not question mark in PG.
That's why it's not working.
So the place orders has to be ordered by number.
Okay, let's try.
How did I know that I was reading the docs?
Let me show you.
NPM.js.com.
We use the PG.
And here you have the documentation.
And scroll down.
You see that is using dollar here, dollar.
And then you can read about the place order.
Think if you call query here.
Let me call somewhere queries here.
See values, dollar one, dollar two.
Usually, order frameworks is question mark.
That's why I got confused.
Anyway, let's go back.
Now we're done with that.
Remove break points, play.
Go back to terminal.
Close this.
Yeah, room dev.
Make that should fix it.
Go to postman.
Try again.
I got okay.
So I think it was created.
Let's remove this.
Go, revert this back, send this back.
And I should see it in PG.
I mean, after 10 here, play.
Hello world.
One, two, three.
So if you just run the website,
let me go to Chrome.
I can see how our words want to be there.
A new message, right?
Let's see the dev tools.
What happened here?
Let's look at how it crashed.
Oh, no, it crashed.
72 is the line.
You can read it in the console here.
Something happened here because of an input returning.
If there's no returning, it doesn't return back the data that was created.
So you have to have like return star.
Save that.
Let's try again.
I've crashed this.
Write message.
There you go.
Now it worked.
Think from the read,
think from the read network.
We got back the title with a new message.
Although it didn't work here because if you refresh, it should be there.
But I think it didn't work because the property name is wrong.
So if I go back to the front end, what is it expecting
when you create a new message here?
By the way, we are add message.
We make the request and then extract the response,
set the new message with created post, which comes with title, right?
But I think it's using body here.
Yeah, so we're all over the place.
We use title in here, but we use body in the other thing.
So we should change this.
Let's change it to body, which is the correct one to match with database.
And then going back to the back end, we change it instead of title.
You want to make that body.
So filter properties, change it to body.
And the request body to body.
And this to body, body dot body.
And change all this stuff.
And then you control F to search more title stuff.
So this should be body.
So everything you see title, we have to say body now.
Okay, I don't find any more title when I do control F to search here,
enter nothing else.
Save that.
Let's try again.
So let's go refresh.
Hi there.
And it's here.
And if I look at the database,
and I click play again, should be the last one is there.
So it's working fine.
No comments yet (loading...)
No comments yet (loading...)
Did you like the lesson? 😆👍
Consider a donation to support our work: