From abedf0769be2354a48d6196651c3ebee58da175f Mon Sep 17 00:00:00 2001 From: magdazelena Date: Mon, 6 Oct 2025 20:22:05 +0200 Subject: [PATCH 01/32] chore: add week 2 legacy material --- courses/backend/node/week2/README.md | 46 +++++ courses/backend/node/week2/assignment.md | 211 +++++++++++++++++++++ courses/backend/node/week2/preparation.md | 11 ++ courses/backend/node/week2/session-plan.md | 60 ++++++ 4 files changed, 328 insertions(+) diff --git a/courses/backend/node/week2/README.md b/courses/backend/node/week2/README.md index e69de29b..0d67f92b 100644 --- a/courses/backend/node/week2/README.md +++ b/courses/backend/node/week2/README.md @@ -0,0 +1,46 @@ +# Session plan (Week 2) + +In this session we will focus on connecting to a database, building an API, and using Postman to test our API endpoints. We will also cover how to structure our code for better maintainability and scalability. + +## Contents + +- [Preparation](./preparation.md) +- [Session Plan](./session-plan.md) (for mentors) +- [Assignment](./assignment.md) + +## Session Learning goals + +By the end of this session, you will be able to: +TODO - Format as `verb` + +- [ ] Database interaction + - [ ] Connecting to mysql using Knex + - [ ] Environment variables + - [ ] Executing queries using knex +- [ ] API + - [ ] REST + - [ ] CRUD + - [ ] Router verb `GET`, `POST`, `DELETE`, `PUT` + - [ ] POST mention express.json middleware + - [ ] Postman + +TODO - Move this content somewhere else + +### 1. What is Representational State Transfer (REST)? + +Building software is like building houses: architecture is everything. The design of each part is just as important as the utility of it. REST is a specific architectural style for web applications. It serves to organise code in **predictable** ways. + +The most important features of REST are: + +- An application has a `frontend` (client) and a `backend` (server). This is called [separation of concerns](https://medium.com/machine-words/separation-of-concerns-1d735b703a60): each section has its specific job to do. The frontend deals with presenting data in a user friendly way, the backend deals with all the logic and data manipulation +- The server is `stateless`, which means that it doesn't store any data about a client session. Whenever a client sends a request to the server, each request from the client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. This makes it possible to handle requests from millions of users. +- Server responses can be temporarily stored on the client (a browser) using a process called `caching`: storing files like images or webpages in the browser to load the next time you enter a website (instead of getting them from the server, which generally takes longer to do). +- Client-server communication is done through `Hypertext Transfer Protocol (HTTP)` (more on that later), which serves as the style (the how) of communication. + +It's important to know about REST because it teaches us how web applications are designed and holds us to a standard that makes development and usage predictable. However, don't worry if you don't know what any of this means just yet. It's good to be exposed to it, and understanding will come with experience. + +For more research, check the following resource: + +- [What is REST: a simple explanation for beginners](https://medium.com/extend/what-is-rest-a-simple-explanation-for-beginners-part-1-introduction-b4a072f8740f) + +- [@NoerGitKat (lots of web app clones/examples to learn from)](https://github.com/NoerGitKat) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index e69de29b..2e67f267 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -0,0 +1,211 @@ +# Assignment + +Once again, you will deliver 2 pull requests: + +- A pull request for the **Warmup** - in your regular hyf-homework repository +- A pull request for the additional **meal sharing endpoints** - in the meal-sharing repository + +In both repositories, create a `nodejs-week2` branch from `main` to work on the homework (`git checkout -b nodejs-week2` ). + +## Warmup + +For the warmup you will be handed a Contacts API with a single endpoint: + +- `GET /api/contacts` + +This endpoint accepts a query parameter `sort`. Here's how you can use it: + +- `GET /api/contacts?sort=first_name%20ASC` + - Sorts contacts by first name, ascending +- `GET /api/contacts?sort=last_name%20DESC` + - Sorts contacts by last name, descending + +But this `sort` query parameter has been introduced with a SQL injection vulnerability and the goal is to demonstrate the issue and then fix and remove the vulnerability. + +### Setup + +TODO - Review assignment to work with sqlite. + +Go to `nodejs/week2` in your `hyf-homework` repo: + +```shell +npm init -y +npm i express mysql2 knex +npm i --save-dev nodemon +npm set-script dev "nodemon app.js" +``` + +Make sure you have `"type": "module"` in your `package.json`. + +You should also ensure that the `node_modules/` folder is ignored by Git: + +```shell +echo node_modules/ >> .gitignore +``` + +Create a database/schema called `hyf_node_week2_warmup` with a `contacts` table: + +```sql +CREATE TABLE `contacts` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `first_name` varchar(255) NOT NULL, + `last_name` varchar(255) NOT NULL, + `email` varchar(255) DEFAULT NULL, + `phone` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- Sample data +insert into contacts (id, first_name, last_name, email, phone) values (1, 'Selig', 'Matussov', 'smatussov0@pinterest.com', '176-630-4577'); +insert into contacts (id, first_name, last_name, email, phone) values (2, 'Kenny', 'Yerrington', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (3, 'Emilie', 'Gaitskell', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (4, 'Jordon', 'Tokell', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (5, 'Sallyann', 'Persse', 'spersse4@webnode.com', '219-157-2368'); +insert into contacts (id, first_name, last_name, email, phone) values (6, 'Berri', 'Bulter', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (7, 'Lanni', 'Ivanilov', 'livanilov6@fda.gov', null); +insert into contacts (id, first_name, last_name, email, phone) values (8, 'Dagny', 'Milnthorpe', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (9, 'Annadiane', 'Bansal', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (10, 'Tawsha', 'Hackley', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (11, 'Rubetta', 'Ozelton', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (12, 'Charles', 'Boughey', 'cbougheyb@senate.gov', '605-358-5664'); +insert into contacts (id, first_name, last_name, email, phone) values (13, 'Shantee', 'Robbe', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (14, 'Gleda', 'Peat', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (15, 'Arlinda', 'Ethersey', 'aetherseye@biglobe.ne.jp', '916-139-1300'); +insert into contacts (id, first_name, last_name, email, phone) values (16, 'Armando', 'Meachem', 'ameachemf@oaic.gov.au', '631-442-5339'); +insert into contacts (id, first_name, last_name, email, phone) values (17, 'Codi', 'Redhouse', null, '401-953-6897'); +insert into contacts (id, first_name, last_name, email, phone) values (18, 'Ann', 'Buncombe', 'abuncombeh@ow.ly', '210-338-0748'); +insert into contacts (id, first_name, last_name, email, phone) values (19, 'Louis', 'Matzkaitis', 'lmatzkaitisi@ebay.com', '583-996-6979'); +insert into contacts (id, first_name, last_name, email, phone) values (20, 'Jessey', 'Pala', null, null); +insert into contacts (id, first_name, last_name, email, phone) values (21, 'Archy', 'Scipsey', 'ascipseyk@ask.com', '420-983-2426'); +insert into contacts (id, first_name, last_name, email, phone) values (22, 'Benoit', 'Mould', 'bmouldl@bing.com', '271-217-9218'); +insert into contacts (id, first_name, last_name, email, phone) values (23, 'Sherm', 'Girardey', 'sgirardeym@guardian.co.uk', '916-999-2957'); +insert into contacts (id, first_name, last_name, email, phone) values (24, 'Raquel', 'Mudge', 'rmudgen@slate.com', '789-830-7473'); +insert into contacts (id, first_name, last_name, email, phone) values (25, 'Tabor', 'Reavey', null, null); +``` + +Create `app.js`: + +```js +import knex from "knex"; +const knexInstance = knex({ + client: "mysql2", + connection: { + host: process.env.DB_HOST || "127.0.0.1", + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER || "root", + password: process.env.DB_PASSWORD || "my-secret-pw", + database: process.env.DB_NAME || "hyf_node_week2_warmup", + multipleStatements: true, + }, +}); + +import express from "express"; +const app = express(); +const port = process.env.PORT || 3000; + +app.use(express.json()); + +const apiRouter = express.Router(); +app.use("/api", apiRouter); + +const contactsAPIRouter = express.Router(); +apiRouter.use("/contacts", contactsAPIRouter); + +contactsAPIRouter.get("/", async (req, res) => { + let query = knexInstance.select("*").from("contacts"); + + if ("sort" in req.query) { + const orderBy = req.query.sort.toString(); + if (orderBy.length > 0) { + query = query.orderByRaw(orderBy); + } + } + + console.log("SQL", query.toSQL().sql); + + try { + const data = await query; + res.json({ data }); + } catch (e) { + console.error(e); + res.status(500).json({ error: "Internal server error" }); + } +}); + +app.listen(port, () => { + console.log(`Listening on port ${port}`); +}); +``` + +As mentioned above, the `sort` query parameter has been introduced with a SQL injection vulnerability. + +First, you should demonstrate the SQL injection and that it for instance is possible to drop/delete the `contacts` table with the `sort` query parameter. +You can for instance demonstrate this with a screen recording and include it in the PR description. + +After having demonstrated the SQL injection vulnerability, the goal is then to fix the issue by updating `app.js`. + +**Hint:** the `multipleStatements: true` part in the configuration indicates how you can use the vulnerability. The configuration should not be changed though, the SQL injection should be fixed by making changes in the `/api/contacts` route. + +## Meal sharing endpoints + +You will continue working in the meal-sharing repository for this task. + +You should have the basic [CRUD](https://www.freecodecamp.org/news/crud-operations-explained/) endpoints for **meals** and **reservations** as the result of last week's homework. This week, you will add **query parameters**, that will allow you to **sort** and **filter** the information retrieved from the database. + +### Routes + +#### Meals + +Work with your `GET api/meals` route to add the query parameters. +Make sure that the query parameters can be combined, f.x. `?limit=4&maxPrice=90`. + +| Parameter | Data type | Description | Example | +| ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| `maxPrice` | Number | Returns all meals that are cheaper than `maxPrice`. | `api/meals?maxPrice=90` | +| `availableReservations` | Boolean | Returns all meals that still have available spots left, if `true`. If `false`, return meals that have no available spots left.[^1] | `api/meals?availableReservations=true` | +| `title` | String | Returns all meals that partially match the given title. `Rød grød` will match the meal with the title `Rød grød med fløde`. | `api/meals?title=Indian%20platter` | +| `dateAfter` | Date | Returns all meals where the date for `when` is after the given date. | `api/meals?dateAfter=2022-10-01` | +| `dateBefore` | Date | Returns all meals where the date for `when` is before the given date. | `api/meals?dateBefore=2022-08-08` | +| `limit` | Number | Returns the given number of meals. | `api/meals?limit=7` | +| `sortKey`[^2] | String | Returns all meals sorted by the given key. Allows `when`, `max_reservations` and `price` as keys. Default sorting order is asc(ending). | `api/meals?sortKey=price` | +| `sortDir`[^3] | String | Returns all meals sorted in the given direction. Only works combined with the `sortKey` and allows `asc` or `desc`. | `api/meals?sortKey=price&sortDir=desc` | + +[^1]: `availableReservations` requires you to work with several database tables at once. Try practicing the right query in MySQL Workbench first (you might have it from Database week2 homework) and once you have it working, build it with `knex`. + +[^2]: This used to be `sort_key` in a previous version of the homework text. + +[^3]: This used to be `sort_dir` in a previous version of the homework text. + +#### Reviews + +By now, you have the basic set of endpoints for **meals** and **reservations** and even a collection of query parameters for **meals**. To practice a bit more and finalize the basic backend functionality, create the set of routes for **reviews**: + +| Route | HTTP method | Description | +| ----------------------------- | ----------- | ---------------------------------------- | +| `/api/reviews` | GET | Returns all reviews. | +| `/api/meals/:meal_id/reviews` | GET | Returns all reviews for a specific meal. | +| `/api/reviews` | POST | Adds a new review to the database. | +| `/api/reviews/:id` | GET | Returns a review by `id`. | +| `/api/reviews/:id` | PUT | Updates the review by `id`. | +| `/api/reviews/:id` | DELETE | Deletes the review by `id`. | + +#### Knex + +You should try to avoid using `knex.raw` and instead use the different `knex` functions, for example: + +- `.select`, `.from`, `.where`, `join`, `leftJoin` +- `.insert` +- `.update` +- `.del` (for deletion) + +Check out the [Knex cheatsheet](https://devhints.io/knex)! + +## Hand in homework + +Need to brush up on the homework hand-in process? + +Check [this resource](https://github.com/HackYourFuture-CPH/Git/blob/main/homework-submission.md) to remember how to hand in the homework correctly! + +## Feedback + +And finally, please take two minutes to answer the survey [here](https://forms.gle/YG5KCnSCPhb8dJAL9) to give feedback to the staff and mentors. diff --git a/courses/backend/node/week2/preparation.md b/courses/backend/node/week2/preparation.md index e69de29b..0a2c6f55 100644 --- a/courses/backend/node/week2/preparation.md +++ b/courses/backend/node/week2/preparation.md @@ -0,0 +1,11 @@ +# Preparation + +- [NodeJS Web API with KNEX and Express](https://www.youtube.com/watch?v=QNw9q4YXR4E) (15 min) +- up until the `The Visual Studio Code REST client` section (15 min) +- - Free API for testing and prototyping. (5 min) + +## Flipped classroom videos + +- [Connecting nodejs to a database using Knex part 1 - Class 3](https://youtu.be/W5xFbiAl4bo) +- [Connecting nodejs to a database using Knex part 2 - Class 3](https://youtu.be/cacTSGU7Hrc) +- [Creating an api using nodejs and express - Class 3](https://youtu.be/i-BUdUMz6Zk) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index e69de29b..972104b6 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -0,0 +1,60 @@ +# Lesson plan + +- Focus on having lots of in class exercises. +- DON'T teach everything, let the students investigate topics on their own as well! +- Focus on how to read documentation, google answers and google errors!! +- Teach towards the students being able to solve the homework. + +Remember to add the code you wrote in the class to the relevant class branch's class work folder. If the branch has not been created just create and push it :) If you don't have access, write to one from the core team. You can see an example below! + +To find examples of what teachers have taught before go to the class branches in the classwork folder, e.g. [class 07](https://github.com/HackYourFuture-CPH/JavaScript/tree/class07/JavaScript1/Week1/classwork) + +If you find anything that could be improved then please create a pull request! We welcome changes, so please get involved if you have any ideas!!! + +--- + +- Database interaction + - Connecting to mysql using knex + - Executing queries + - `select`, `create`. You could let the students figure out how `delete` and `update` works + - [Code inspiration](#phonebook-database) especially focus on the promise and query part +- API + - REST + - CRUD + - Router verb `GET`, `POST`, `DELETE`, `PUT` + - Especially focus on post with `app.use(express.urlencoded({ extended: true }));` and `app.use(express.json());` + - [Code inspiration](#phonebook-api) +- Postman + - `POST`, `DELETE`, `PUT` requests +- Exercise finish concerts api + +## Flipped classroom videos + +[Flipped classroom videos](https://github.com/HackYourFuture-CPH/node.js/blob/main/week1/preparation.md#flipped-classroom-videos) + +## Code inspiration + +### Phonebook database + +- Go to the `teacher-live-coding` [repo](https://github.com/HackYourFuture-CPH/teacher-live-coding), to the relevant folder +- Copy the `.env.example` and rename the copied file to `.env` +- Run `npm install` +- Start the application by running `nodemon ./src/backend/phonebook-database-queries.js` + +Try and implement this functionality from the bottom while explaining. + +### Phonebook api + +Start the application by running `nodemon ./src/backend/create-an-api.js`. + +The following two routes have been created, get help by the students to create some of the other routes. + +| Url | Verb | Functionality | Example | +| ------------------- | ------ | --------------------------- | -------------------- | +| `api/contacts/` | GET | Returns all contacts | `GET api/contacts/` | +| `api/contacts/` | POST | Adds a new contact | `POST api/contacts/` | +| `api/contacts/{id}` | GET | Returns contact by `id` | `GET api/contacts/2` | +| `api/contacts/{id}` | PUT | Updates the contact by `id` | `PUT api/contacts/2` | +| `api/contacts/{id}` | DELETE | Deletes the contact by `id` | `DELETE contacts/2` | + +Thank you very much for teaching NodeJS. Please don't hesitate to give feedback by clicking [here](https://forms.gle/sAuVhsTmJ1qSmjgJ6) (teachers and teacher assistants). For homework reviewers, please access the survey [here](https://forms.gle/nVbX9ShusF2a5Aa87). From 7a7d46c6083413df613f15e20994499e3cf2308f Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 7 Oct 2025 14:52:55 +0200 Subject: [PATCH 02/32] Expand on some of the learning goals --- courses/backend/node/week2/README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/courses/backend/node/week2/README.md b/courses/backend/node/week2/README.md index 0d67f92b..90582582 100644 --- a/courses/backend/node/week2/README.md +++ b/courses/backend/node/week2/README.md @@ -1,4 +1,4 @@ -# Session plan (Week 2) +# Node (Week 2) In this session we will focus on connecting to a database, building an API, and using Postman to test our API endpoints. We will also cover how to structure our code for better maintainability and scalability. @@ -11,18 +11,21 @@ In this session we will focus on connecting to a database, building an API, and ## Session Learning goals By the end of this session, you will be able to: -TODO - Format as `verb` +TODO - Format as `verb` for API section -- [ ] Database interaction - - [ ] Connecting to mysql using Knex - - [ ] Environment variables - - [ ] Executing queries using knex +- [ ] Learn how to manage advanced database interactions in your service + - [ ] Set up a connection to your mysql database using Knex + - [ ] Configure environment variables + - [ ] Execute `select`, `create`, `delete` and `update` queries using Knex - [ ] API - [ ] REST - [ ] CRUD - [ ] Router verb `GET`, `POST`, `DELETE`, `PUT` - [ ] POST mention express.json middleware - - [ ] Postman + - [ ] Configure Postman for advanced backend development + - [ ] Set up multiple environments + - [ ] Manage secrets + - [ ] Create basic test suites TODO - Move this content somewhere else From e2a2a25705044c391e01085973a503cac956336d Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 7 Oct 2025 14:54:41 +0200 Subject: [PATCH 03/32] Removed some old legacy paragraphs --- courses/backend/node/week2/assignment.md | 10 ---------- courses/backend/node/week2/session-plan.md | 2 -- 2 files changed, 12 deletions(-) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index 2e67f267..1a299c49 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -199,13 +199,3 @@ You should try to avoid using `knex.raw` and instead use the different `knex` fu - `.del` (for deletion) Check out the [Knex cheatsheet](https://devhints.io/knex)! - -## Hand in homework - -Need to brush up on the homework hand-in process? - -Check [this resource](https://github.com/HackYourFuture-CPH/Git/blob/main/homework-submission.md) to remember how to hand in the homework correctly! - -## Feedback - -And finally, please take two minutes to answer the survey [here](https://forms.gle/YG5KCnSCPhb8dJAL9) to give feedback to the staff and mentors. diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 972104b6..10dd96e0 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -56,5 +56,3 @@ The following two routes have been created, get help by the students to create s | `api/contacts/{id}` | GET | Returns contact by `id` | `GET api/contacts/2` | | `api/contacts/{id}` | PUT | Updates the contact by `id` | `PUT api/contacts/2` | | `api/contacts/{id}` | DELETE | Deletes the contact by `id` | `DELETE contacts/2` | - -Thank you very much for teaching NodeJS. Please don't hesitate to give feedback by clicking [here](https://forms.gle/sAuVhsTmJ1qSmjgJ6) (teachers and teacher assistants). For homework reviewers, please access the survey [here](https://forms.gle/nVbX9ShusF2a5Aa87). From 0447024f1ba03f8bdb3c0e18992c6b5af36d8fce Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 28 Oct 2025 14:26:42 +0100 Subject: [PATCH 04/32] Tidy up and add postman session plan notes and exercises --- .../node/week1/session-materials/06-auth.md | 2 +- courses/backend/node/week1/session-plan.md | 2 +- courses/backend/node/week2/README.md | 3 +- courses/backend/node/week2/session-plan.md | 89 +++++++++++++------ 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/courses/backend/node/week1/session-materials/06-auth.md b/courses/backend/node/week1/session-materials/06-auth.md index 57ff25e8..cb0dc187 100644 --- a/courses/backend/node/week1/session-materials/06-auth.md +++ b/courses/backend/node/week1/session-materials/06-auth.md @@ -116,4 +116,4 @@ It is left as an optional exercise to add the following routes: - `PUT /api/snippets/:id` to update a snippet - `DELETE /api/snippets/:id` to delete a snippet -Also, it could be a good idea to deny the request if the user making the request is not confirmed +Also, it could be a good idea to deny the request if the user making the request is not confirmed. diff --git a/courses/backend/node/week1/session-plan.md b/courses/backend/node/week1/session-plan.md index 6b2f1946..756d0c28 100644 --- a/courses/backend/node/week1/session-plan.md +++ b/courses/backend/node/week1/session-plan.md @@ -1,6 +1,6 @@ # Session plan -## Session Outline +## Session outline - Express - What is Express (10 mins) diff --git a/courses/backend/node/week2/README.md b/courses/backend/node/week2/README.md index 90582582..4a69996c 100644 --- a/courses/backend/node/week2/README.md +++ b/courses/backend/node/week2/README.md @@ -23,8 +23,9 @@ TODO - Format as `verb` for API section - [ ] Router verb `GET`, `POST`, `DELETE`, `PUT` - [ ] POST mention express.json middleware - [ ] Configure Postman for advanced backend development + - [ ] Creating collections and saving requests - [ ] Set up multiple environments - - [ ] Manage secrets + - [ ] Managing secrets - [ ] Create basic test suites TODO - Move this content somewhere else diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 10dd96e0..2cc239a1 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -1,17 +1,6 @@ -# Lesson plan +# Session plan -- Focus on having lots of in class exercises. -- DON'T teach everything, let the students investigate topics on their own as well! -- Focus on how to read documentation, google answers and google errors!! -- Teach towards the students being able to solve the homework. - -Remember to add the code you wrote in the class to the relevant class branch's class work folder. If the branch has not been created just create and push it :) If you don't have access, write to one from the core team. You can see an example below! - -To find examples of what teachers have taught before go to the class branches in the classwork folder, e.g. [class 07](https://github.com/HackYourFuture-CPH/JavaScript/tree/class07/JavaScript1/Week1/classwork) - -If you find anything that could be improved then please create a pull request! We welcome changes, so please get involved if you have any ideas!!! - ---- +## Session outline - Database interaction - Connecting to mysql using knex @@ -28,10 +17,6 @@ If you find anything that could be improved then please create a pull request! W - `POST`, `DELETE`, `PUT` requests - Exercise finish concerts api -## Flipped classroom videos - -[Flipped classroom videos](https://github.com/HackYourFuture-CPH/node.js/blob/main/week1/preparation.md#flipped-classroom-videos) - ## Code inspiration ### Phonebook database @@ -43,16 +28,66 @@ If you find anything that could be improved then please create a pull request! W Try and implement this functionality from the bottom while explaining. -### Phonebook api +## Postman + +Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. + +### 1. Creating collections and saving requests + +Collections let you group related requests into a reusable library. This makes it easy to organize, run, and share sets of API calls. Read more on the [Official docs](https://learning.postman.com/docs/collections/collections-overview/). + +#### Exercise +Create a collection for your Snippets API. Add an unauthenticated `GET /api/snippets` request, and save it to the collection. + +1. In Postman, click **New → Collection**, and give it a meaningful name and description. +2. On the collection, click the **+** icon and create the GET request. Give it a meaningful name and **Save** it to the collection, once it's working. + +### 2. Set Up Multiple Environments + +Environments in Postman let you define sets of variables (e.g. base URLs, tokens) for different contexts. That could be your local environment, staging, and production. Switching environments changes the variable values used in your requests. +Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/variables/managing-environments/). + +#### Exercise +1. In Postman, go to **Environments** (top-right environment selector) and click **Add**. +2. Name your environment your first environment `Local` +3. Add a variable called `base_url` and set it to `http://localhost:3000` (or wherver you're running your local server). +4. Update your requests to use them in the URL, like: `{{base_url}}/api/snippets`. +5. Select the environment in the dropdown to apply its variables to all requests you run. + +All details you place in variables are local by default. For additional security, mark them as sensitive, if you keep secrets or passwords in here. + +In the future, when you come to deploy your app to the web, you can create a new environment in the same way called `Production` and recreate the same variables with updated values for your deployed app. Then you can easily switch between them in Postman to test both local and production versions of the APIs. + +### 3. Managing Secrets + +You'll often need to use sensitive data in your requests, namely secrets (API keys, passwords, tokens). These should not be hard-coded in your requests for security reasons! Postman provides a **Vault** and sensitive variable settings to securely store and reuse secrets. Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/postman-vault/postman-vault-secrets/). + +#### Exercise +Let's add the authenticated request to `GET /api/snippets`, so we can test returning private snippets. + +1. Open the **Postman Vault** (Vault icon or `Ctrl+Shift+V`). +2. When prompted, Postman generates a vault key - save it securely. +3. Add a new secret called `users_token` with a value from the `users.token` column in your database. +4. Now you're ready to use it in a request! Create a new GET request as you did in the first exercise, but this time add an authorization header. Where you need to reference your token, use `{{vault:users_token}}`. + +Now you can safely and securely test APIs using secrets. Test to make sure the request is working correctly, and save it to your collection. + +### 4. Create Basic Test Suites -Start the application by running `nodemon ./src/backend/create-an-api.js`. +Postman allows you to write test scripts (in JavaScript) that validate your API responses — checking status codes, payloads, and performance. These tests can be grouped into collections and run automatically. They are a handy way to validate that your API is working correctly, and continues to work correctly as you make changes. Read more on the [Official docs](https://learning.postman.com/docs/tests-and-scripts/write-scripts/test-scripts/). -The following two routes have been created, get help by the students to create some of the other routes. +#### Exercise +1. In a request, open the **Tests** tab. +2. Write assertions using `pm.test()` and the `pm.response` object. Here are two examples you can paste in: +```javascript +pm.test("Status code is 200", function () { + pm.response.to.have.status(200); +}); -| Url | Verb | Functionality | Example | -| ------------------- | ------ | --------------------------- | -------------------- | -| `api/contacts/` | GET | Returns all contacts | `GET api/contacts/` | -| `api/contacts/` | POST | Adds a new contact | `POST api/contacts/` | -| `api/contacts/{id}` | GET | Returns contact by `id` | `GET api/contacts/2` | -| `api/contacts/{id}` | PUT | Updates the contact by `id` | `PUT api/contacts/2` | -| `api/contacts/{id}` | DELETE | Deletes the contact by `id` | `DELETE contacts/2` | +pm.test("Snippet has a numeric 'id' field", function () { + const json = pm.response.json(); + pm.expect(json.id).to.be.a("number"); +}); +``` +3. Write at least one additional test case for both of your requests. +4. These tests will run automatically each time you send the request. You can also run them all together, like a test suite. Click on your collection, and find the "Run" button. Make sure all the requests are checked that you wish to test. Click run, and a report will display all of test results. From 7cd05160b988a1fb433ccc4708a5501f983000e2 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 28 Oct 2025 14:31:33 +0100 Subject: [PATCH 05/32] Added weeks and docs to node module for gitbook --- SUMMARY.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SUMMARY.md b/SUMMARY.md index e83ea34e..2fb2c1eb 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -115,6 +115,14 @@ - [Session Plan](courses/backend/databases/week2/session-plan.md) - [Assignment](courses/backend/databases/week2/assignment.md) - [Node](courses/backend/node/README.md) + - [Week 1](courses/backend/node/week1/README.md) + - [Preparation](courses/backend/node/week1/preparation.md) + - [Session Plan](courses/backend/node/week1/session-plan.md) + - [Assignment](courses/backend/node/week1/assignment.md) + - [Week 2](courses/backend/node/week2/README.md) + - [Preparation](courses/backend/node/week2/preparation.md) + - [Session Plan](courses/backend/node/week2/session-plan.md) + - [Assignment](courses/backend/node/week2/assignment.md) - [Final Backend Project](courses/backend/final-project/README.md) - [Common Modules](shared-modules/README.md) From 957204b2e9d7bd26de787383bd022fb1c6d45752 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 28 Oct 2025 14:35:00 +0100 Subject: [PATCH 06/32] fixed some gitbook links --- courses/backend/node/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/courses/backend/node/README.md b/courses/backend/node/README.md index ddd5ab78..f1c3e615 100644 --- a/courses/backend/node/README.md +++ b/courses/backend/node/README.md @@ -4,10 +4,10 @@ This module is part of the Backend specialism and focuses on using Node.js to bu ## Contents -| Week | Topic | Preparation | Lesson Plan | Assignment | +| Week | Topic | Preparation | Session Plan | Assignment | | ---- | ------------------------ | ----------------------------------- | ----------------------------------- | ------------------------------------- | -| 1. | Express | [Preparation](week1/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](week1/session-plan.md) | -| 2. | Database connection; API | [Preparation](week2/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](week2/session-plan.md) | +| 1. | Express | [Preparation](./week1/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](./week1/session-plan.md) | +| 2. | Database connection; API | [Preparation](./week2/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](./week2/session-plan.md) | ## Module Learning Goals From ee2163a4a5dd3061a0c0a4c81dec2c1622c83228 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 28 Oct 2025 14:45:00 +0100 Subject: [PATCH 07/32] tidied up where postman is mentioned, and added a todo idea for assignment task --- courses/backend/node/README.md | 2 +- courses/backend/node/week1/README.md | 1 - courses/backend/node/week2/assignment.md | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/courses/backend/node/README.md b/courses/backend/node/README.md index f1c3e615..3473db15 100644 --- a/courses/backend/node/README.md +++ b/courses/backend/node/README.md @@ -16,6 +16,6 @@ By the end of this module, you will be able to: - [ ] Build web servers with Express.js - [ ] Design and implement APIs using HTTP methods following REST principles - [ ] Use middlewares for authentication, logging, and validation -- [ ] Test APIs using Postman - [ ] Use logging and debugging tools to monitor and troubleshoot applications - [ ] Connect to databases and implement CRUD operations +- [ ] Test APIs using Postman diff --git a/courses/backend/node/week1/README.md b/courses/backend/node/week1/README.md index 457c425d..b1966286 100644 --- a/courses/backend/node/week1/README.md +++ b/courses/backend/node/week1/README.md @@ -16,4 +16,3 @@ By the end of this session, you will be able to: - [ ] Implement routing in Express to handle different HTTP requests and endpoints. - [ ] Use logging and debugging tools to monitor and troubleshoot Node.js applications. - [ ] Apply middleware functions in Express to process requests and responses. -- [ ] Use Postman to test and debug APIs you have built. diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index 1a299c49..d3188c42 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -7,6 +7,8 @@ Once again, you will deliver 2 pull requests: In both repositories, create a `nodejs-week2` branch from `main` to work on the homework (`git checkout -b nodejs-week2` ). +TODO - We should add a task to practice postman too. Maybe add all their final endpoints to their collection. + ## Warmup For the warmup you will be handed a Contacts API with a single endpoint: From 826fc83969bf203988d436741ab681d2b2cfaeed Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:02:01 +0100 Subject: [PATCH 08/32] added phonebook examples and content to teach knex and refresh crud --- courses/backend/node/week1/session-plan.md | 47 +++++++++---------- .../session-materials/phonebook/database.js | 13 +++++ .../session-materials/phonebook/phonebook.js | 37 +++++++++++++++ .../session-materials/phonebook/phonebook.sql | 14 ++++++ courses/backend/node/week2/session-plan.md | 44 ++++++++++++----- 5 files changed, 119 insertions(+), 36 deletions(-) create mode 100644 courses/backend/node/week2/session-materials/phonebook/database.js create mode 100644 courses/backend/node/week2/session-materials/phonebook/phonebook.js create mode 100644 courses/backend/node/week2/session-materials/phonebook/phonebook.sql diff --git a/courses/backend/node/week1/session-plan.md b/courses/backend/node/week1/session-plan.md index 756d0c28..16572a1c 100644 --- a/courses/backend/node/week1/session-plan.md +++ b/courses/backend/node/week1/session-plan.md @@ -2,30 +2,29 @@ ## Session outline -- Express - - What is Express (10 mins) - - [Live coding: setup a server](./session-materials/01-server.md) - - [Excercise: create a local project and database schema](./session-materials/02-schema.md) - - Routing in Express (20 mins) - - `app.use` - - `app.get` - - [Live coding: routing](#appget-vs-appuse) - - [Excercise: Setup routing](./session-materials/03-routing.md) (10 mins) - - URL parameters in Express (30 mins) - - [Explanation and live coding](#query-parameters-vs-url-parameters) - - [Excercise: connect to the database](./session-materials/04-database-connection.md) - - [Excercise: GET endpoints](./session-materials/05-get-endpoints.md) - - Route order (15 mins) - - [Live coding: why route order matters](#route-order) - - Logging and debugging - - Middleware (15 mins) - - [`next` method](https://expressjs.com/en/guide/using-middleware.html) - - Modifying `request` and `response` - - - - [Live coding: basic middleware example](#middleware) - - Authentication (30 mins) - - [Authentication explanation](#authentication-explanation) - - [Excercise: implement authentication](./session-materials/06-auth.md) +- What is Express (10 mins) + - [Live coding: setup a server](./session-materials/01-server.md) + - [Excercise: create a local project and database schema](./session-materials/02-schema.md) +- Routing in Express (20 mins) + - `app.use` + - `app.get` + - [Live coding: routing](#appget-vs-appuse) + - [Excercise: Setup routing](./session-materials/03-routing.md) (10 mins) +- URL parameters in Express (30 mins) + - [Explanation and live coding](#query-parameters-vs-url-parameters) + - [Excercise: connect to the database](./session-materials/04-database-connection.md) + - [Excercise: GET endpoints](./session-materials/05-get-endpoints.md) +- Route order (15 mins) + - [Live coding: why route order matters](#route-order) + - Logging and debugging +- Middleware (15 mins) + - [`next` method](https://expressjs.com/en/guide/using-middleware.html) + - Modifying `request` and `response` + - + - [Live coding: basic middleware example](#middleware) +- Authentication (30 mins) + - [Authentication explanation](#authentication-explanation) + - [Excercise: implement authentication](./session-materials/06-auth.md) ## Exercises diff --git a/courses/backend/node/week2/session-materials/phonebook/database.js b/courses/backend/node/week2/session-materials/phonebook/database.js new file mode 100644 index 00000000..2824e63f --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/database.js @@ -0,0 +1,13 @@ +import knex from "knex"; + +// Run "sqlite3 phonebook.sqlite3 < phonebook.sql" to create this database first +const dbFile = "./phonebook.sqlite3"; + +const knexInstance = knex({ + client: "sqlite3", + connection: { + filename: dbFile, + }, +}); + +export default knexInstance; \ No newline at end of file diff --git a/courses/backend/node/week2/session-materials/phonebook/phonebook.js b/courses/backend/node/week2/session-materials/phonebook/phonebook.js new file mode 100644 index 00000000..e4c2a09f --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/phonebook.js @@ -0,0 +1,37 @@ +import knex from "./database.js"; + +// Show https://devhints.io/knex for a useful cheatsheet of knex comamands + +// Create +const createContact = async (name, phonenumber) => { + const [id] = await knex('contacts').insert({ name, phonenumber }); + return id; +}; + +// Read (all) +const getContacts = async () => { + return await knex('contacts').select('*'); +}; + +// Read (by id) +const getContactById = async (id) => { + return await knex('contacts').where({ id }).first(); +}; + +// Update +const updateContact = async (id, updates) => { + await knex('contacts').where({ id }).update(updates); + return await getContactById(id); // return updated record +};b + +// Delete +const deleteUser = async (id) => { + await knex('contacts').where({ id }).del(); + return { deleted: true }; +}; + +const main = async () => { + // Call your functions here to test them +} + +main().catch(console.error); diff --git a/courses/backend/node/week2/session-materials/phonebook/phonebook.sql b/courses/backend/node/week2/session-materials/phonebook/phonebook.sql new file mode 100644 index 00000000..5aad68dd --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/phonebook.sql @@ -0,0 +1,14 @@ +-- This file contains the initial data for the phonebook database. +-- It is intended to be used with SQLite. +-- You can execute this SQL statements to bootstrap the database. + +-- Enable foreign keys (must be done at runtime in SQLite) +PRAGMA foreign_keys = ON; + +CREATE TABLE `contacts` ( + `id` INTEGER PRIMARY KEY, + `name` varchar(255) NOT NULL, + `phonenumber` varchar(255) NULL +); + +INSERT INTO contacts (id, name, email, phone) values (1, 'Brandon Bean', '22334455'); diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 2cc239a1..4da3ba82 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -2,20 +2,38 @@ ## Session outline -- Database interaction - - Connecting to mysql using knex - - Executing queries - - `select`, `create`. You could let the students figure out how `delete` and `update` works - - [Code inspiration](#phonebook-database) especially focus on the promise and query part +- Database interaction with Knex + - What is Knex and why are we using it + - Raw vs Query Builder methods + - Configuring Knex + - A refresher on CRUD + - `select`, `create`, `delete` and `update` with Knex + - [Code inspiration](#phonebook-database) especially focus on the promise and query part - API - REST - - CRUD - - Router verb `GET`, `POST`, `DELETE`, `PUT` - - Especially focus on post with `app.use(express.urlencoded({ extended: true }));` and `app.use(express.json());` - - [Code inspiration](#phonebook-api) -- Postman - - `POST`, `DELETE`, `PUT` requests -- Exercise finish concerts api + - `POST`, `DELETE`, `PUT` requests +- Advanced Postman use cases + - Collections, environments, secrets, test suites + +Exercises overview: +1. Postman (written below) + + +## Database interaction with Knex +Trainees have used Knex before. In foundation, they used it with the .raw() command to execute SQL easily. And they also used it last week when learning about Express. + +This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. + +1. Explain what Knex is in more detail, and why we are using it here +2. Compare using .raw() to the Query Builder methods + +### Live coding +Run through the [phonebook example](./session-materials/phonebook/). The code is already written, but feel free to clear some of the functions so you can write them together in the session. + +1. Walk through `database.js` and explain the setup +2. Create the database following the instructions in `database.js` +3. Run through `phonebook.js` function by function, pointing to the documentation which explains all the query builder methods. +4. After writing each function, test it by running it via the main function. ## Code inspiration @@ -32,6 +50,8 @@ Try and implement this functionality from the bottom while explaining. Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. +Note: These exericses could be demod by the mentor, or left to the trainees to do on their own machines. Or a mix of both, whatever feels natural. + ### 1. Creating collections and saving requests Collections let you group related requests into a reusable library. This makes it easy to organize, run, and share sets of API calls. Read more on the [Official docs](https://learning.postman.com/docs/collections/collections-overview/). From 8dfeb51cfd000303b45e44ab8fd3f8a332af01b3 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:02:54 +0100 Subject: [PATCH 09/32] removed old phonebook instructions --- courses/backend/node/week2/session-plan.md | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 4da3ba82..322e9a7b 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -9,16 +9,11 @@ - A refresher on CRUD - `select`, `create`, `delete` and `update` with Knex - [Code inspiration](#phonebook-database) especially focus on the promise and query part -- API - - REST - - `POST`, `DELETE`, `PUT` requests +- Express API continued + - `POST`, `DELETE`, `PUT` requests - Advanced Postman use cases - Collections, environments, secrets, test suites -Exercises overview: -1. Postman (written below) - - ## Database interaction with Knex Trainees have used Knex before. In foundation, they used it with the .raw() command to execute SQL easily. And they also used it last week when learning about Express. @@ -35,17 +30,6 @@ Run through the [phonebook example](./session-materials/phonebook/). The code is 3. Run through `phonebook.js` function by function, pointing to the documentation which explains all the query builder methods. 4. After writing each function, test it by running it via the main function. -## Code inspiration - -### Phonebook database - -- Go to the `teacher-live-coding` [repo](https://github.com/HackYourFuture-CPH/teacher-live-coding), to the relevant folder -- Copy the `.env.example` and rename the copied file to `.env` -- Run `npm install` -- Start the application by running `nodemon ./src/backend/phonebook-database-queries.js` - -Try and implement this functionality from the bottom while explaining. - ## Postman Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. From ed49536da10b9d88062cfd8662ee9bff16797f0f Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:11:22 +0100 Subject: [PATCH 10/32] Renamed post enpoint exercise to continue the correct sequence --- ...4-post-endpoint.md => 07-post-endpoint.md} | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) rename courses/backend/node/week2/session-materials/{04-post-endpoint.md => 07-post-endpoint.md} (57%) diff --git a/courses/backend/node/week2/session-materials/04-post-endpoint.md b/courses/backend/node/week2/session-materials/07-post-endpoint.md similarity index 57% rename from courses/backend/node/week2/session-materials/04-post-endpoint.md rename to courses/backend/node/week2/session-materials/07-post-endpoint.md index a30b90b6..9f74682a 100644 --- a/courses/backend/node/week2/session-materials/04-post-endpoint.md +++ b/courses/backend/node/week2/session-materials/07-post-endpoint.md @@ -1,34 +1,20 @@ -# API +# POST endpoint ## `POST /api/snippets` Let's start with a simplified version of the `POST /api/snippets` route. First we add the POST route to `api/snippets.js`: ```js -// Contents of api/snippets.js - -import express from "express"; -import knex from "../database.js"; - -const router = express.Router(); - -// GET /api/snippets -router.get("/", async (request, response) => { - // TODO -}); +// ... // POST /api/snippets router.post("/", async (request, response) => { // TODO }); -// TODO: GET /api/snippets/:id - -export default router; +// ... ``` ---- - To be able to insert a row into the `snippets` table, we need to have data in the `users` table. Create a user and note what the user ID is. The POST request we want to make will look something like this: @@ -42,9 +28,9 @@ POST /api/snippets } ``` ---- +### Exercise: Implement the POST endpoint -**Task:** when we now make a request like +When we now make a request like: ```text POST /api/snippets @@ -57,7 +43,4 @@ POST /api/snippets you should insert a new row into the `snippets` table with the data from the request body. -Hints: - -- [Insert with Knex](https://knexjs.org/guide/query-builder.html#insert) -- When creating a snippet we also need to specify a `user_id`. For now, you can just pass in the `user_id` in the request body (alongside the other snippet data) +When creating a snippet we also need to specify a `user_id`. For now, you can just pass in the `user_id` in the request body (alongside the other snippet data). From 3133f6567fab2f8905f10c3577b7a6aca56adaa2 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:15:35 +0100 Subject: [PATCH 11/32] added placeholders for put and delete exercises --- .../backend/node/week2/session-materials/08-put-endpoint.md | 3 +++ .../backend/node/week2/session-materials/09-delete-endpoint.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 courses/backend/node/week2/session-materials/08-put-endpoint.md create mode 100644 courses/backend/node/week2/session-materials/09-delete-endpoint.md diff --git a/courses/backend/node/week2/session-materials/08-put-endpoint.md b/courses/backend/node/week2/session-materials/08-put-endpoint.md new file mode 100644 index 00000000..db0ece7f --- /dev/null +++ b/courses/backend/node/week2/session-materials/08-put-endpoint.md @@ -0,0 +1,3 @@ +# PUT endpoint + +TODO diff --git a/courses/backend/node/week2/session-materials/09-delete-endpoint.md b/courses/backend/node/week2/session-materials/09-delete-endpoint.md new file mode 100644 index 00000000..cc83bcee --- /dev/null +++ b/courses/backend/node/week2/session-materials/09-delete-endpoint.md @@ -0,0 +1,3 @@ +# DELETE endpoint + +TODO From ae233a6dd4aab711444f9817dfa3befe6cdc4042 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:25:38 +0100 Subject: [PATCH 12/32] Linked placeholder todos --- courses/backend/node/week2/session-plan.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 322e9a7b..9cb29a6a 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -9,20 +9,23 @@ - A refresher on CRUD - `select`, `create`, `delete` and `update` with Knex - [Code inspiration](#phonebook-database) especially focus on the promise and query part -- Express API continued +- Express APIs continued - `POST`, `DELETE`, `PUT` requests - Advanced Postman use cases - Collections, environments, secrets, test suites ## Database interaction with Knex + Trainees have used Knex before. In foundation, they used it with the .raw() command to execute SQL easily. And they also used it last week when learning about Express. This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. +TODO write some more content for these two bullet points 1. Explain what Knex is in more detail, and why we are using it here 2. Compare using .raw() to the Query Builder methods ### Live coding + Run through the [phonebook example](./session-materials/phonebook/). The code is already written, but feel free to clear some of the functions so you can write them together in the session. 1. Walk through `database.js` and explain the setup @@ -30,6 +33,17 @@ Run through the [phonebook example](./session-materials/phonebook/). The code is 3. Run through `phonebook.js` function by function, pointing to the documentation which explains all the query builder methods. 4. After writing each function, test it by running it via the main function. +## Snippets API continued + +Now we can pick up where we left the exercises last week. Help the trainees complete the remaining endpoints: + +TODO: Add a section with live coding somewhere below to explain error handling? include that in one/all of the exercises? + +1. [POST endpoint exercise](./session-materials/07-post-endpoint.md) +2. [PUT endpoint exercise](./session-materials/08-put-endpoint.md) +3. [DELETE endpoint exercise](./session-materials/09-delete-endpoint.md) + + ## Postman Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. From e160bbbe9aff5fa02390a7f966f38bfd8813d29f Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:35:09 +0100 Subject: [PATCH 13/32] Tidied up learning goals and session plans --- courses/backend/node/week1/README.md | 3 ++- courses/backend/node/week2/README.md | 31 +++++++++------------- courses/backend/node/week2/session-plan.md | 19 ++++--------- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/courses/backend/node/week1/README.md b/courses/backend/node/week1/README.md index b1966286..bae8da0b 100644 --- a/courses/backend/node/week1/README.md +++ b/courses/backend/node/week1/README.md @@ -13,6 +13,7 @@ In this session we will focus on Express.js, which is an application framework f By the end of this session, you will be able to: - [ ] Explain what Express is and describe why it is used for building backend applications. -- [ ] Implement routing in Express to handle different HTTP requests and endpoints. +- [ ] Implement routing in Express to handle `GET` HTTP requests, endpoints and parameters. - [ ] Use logging and debugging tools to monitor and troubleshoot Node.js applications. - [ ] Apply middleware functions in Express to process requests and responses. +- [ ] Understand the importance of Authentication and various methods for implementing it. diff --git a/courses/backend/node/week2/README.md b/courses/backend/node/week2/README.md index 4a69996c..1cae7746 100644 --- a/courses/backend/node/week2/README.md +++ b/courses/backend/node/week2/README.md @@ -11,24 +11,19 @@ In this session we will focus on connecting to a database, building an API, and ## Session Learning goals By the end of this session, you will be able to: -TODO - Format as `verb` for API section - -- [ ] Learn how to manage advanced database interactions in your service - - [ ] Set up a connection to your mysql database using Knex - - [ ] Configure environment variables - - [ ] Execute `select`, `create`, `delete` and `update` queries using Knex -- [ ] API - - [ ] REST - - [ ] CRUD - - [ ] Router verb `GET`, `POST`, `DELETE`, `PUT` - - [ ] POST mention express.json middleware - - [ ] Configure Postman for advanced backend development - - [ ] Creating collections and saving requests - - [ ] Set up multiple environments - - [ ] Managing secrets - - [ ] Create basic test suites - -TODO - Move this content somewhere else + +- [ ] Manage advanced database interactions in your service + - [ ] Understand what Knex is and why to use it + - [ ] Set up connections to your database using Knex + - [ ] Execute `select`, `create`, `delete` and `update` queries using Knex Query Builder +- [ ] Implement all REST endpoints using Express + - [ ] `POST`, `DELETE`, `PUT` + - [ ] Use appropriate error handling to understand and debug issues +- [ ] Configure Postman for advanced backend development + - [ ] Creating collections and saving requests + - [ ] Set up multiple environments + - [ ] Managing secrets + - [ ] Create basic test suites ### 1. What is Representational State Transfer (REST)? diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 9cb29a6a..6428e379 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -2,17 +2,9 @@ ## Session outline -- Database interaction with Knex - - What is Knex and why are we using it - - Raw vs Query Builder methods - - Configuring Knex - - A refresher on CRUD - - `select`, `create`, `delete` and `update` with Knex - - [Code inspiration](#phonebook-database) especially focus on the promise and query part -- Express APIs continued - - `POST`, `DELETE`, `PUT` requests -- Advanced Postman use cases - - Collections, environments, secrets, test suites +- [Database interaction with Knex](#database-interaction-with-knex) +- [Snippets API continued](#snippets-api-continued) +- [Advanced Postman use cases](#advanced-postman) ## Database interaction with Knex @@ -20,9 +12,9 @@ Trainees have used Knex before. In foundation, they used it with the .raw() comm This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. -TODO write some more content for these two bullet points 1. Explain what Knex is in more detail, and why we are using it here 2. Compare using .raw() to the Query Builder methods +(TODO write some more content for these two bullet points to guide the mentor) ### Live coding @@ -43,8 +35,7 @@ TODO: Add a section with live coding somewhere below to explain error handling? 2. [PUT endpoint exercise](./session-materials/08-put-endpoint.md) 3. [DELETE endpoint exercise](./session-materials/09-delete-endpoint.md) - -## Postman +## Advanced Postman Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. From db71b6605bd9fcdd4933d979da21ed16a04221e8 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:37:38 +0100 Subject: [PATCH 14/32] Fixed linting --- courses/backend/node/README.md | 4 +-- .../session-materials/phonebook/database.js | 2 +- .../session-materials/phonebook/phonebook.js | 15 ++++---- courses/backend/node/week2/session-plan.md | 36 +++++++++++-------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/courses/backend/node/README.md b/courses/backend/node/README.md index 3473db15..1415770e 100644 --- a/courses/backend/node/README.md +++ b/courses/backend/node/README.md @@ -4,8 +4,8 @@ This module is part of the Backend specialism and focuses on using Node.js to bu ## Contents -| Week | Topic | Preparation | Session Plan | Assignment | -| ---- | ------------------------ | ----------------------------------- | ----------------------------------- | ------------------------------------- | +| Week | Topic | Preparation | Session Plan | Assignment | +| ---- | ------------------------ | ------------------------------------- | ----------------------------------- | --------------------------------------- | | 1. | Express | [Preparation](./week1/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](./week1/session-plan.md) | | 2. | Database connection; API | [Preparation](./week2/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](./week2/session-plan.md) | diff --git a/courses/backend/node/week2/session-materials/phonebook/database.js b/courses/backend/node/week2/session-materials/phonebook/database.js index 2824e63f..0f51f23b 100644 --- a/courses/backend/node/week2/session-materials/phonebook/database.js +++ b/courses/backend/node/week2/session-materials/phonebook/database.js @@ -10,4 +10,4 @@ const knexInstance = knex({ }, }); -export default knexInstance; \ No newline at end of file +export default knexInstance; diff --git a/courses/backend/node/week2/session-materials/phonebook/phonebook.js b/courses/backend/node/week2/session-materials/phonebook/phonebook.js index e4c2a09f..e2a79c52 100644 --- a/courses/backend/node/week2/session-materials/phonebook/phonebook.js +++ b/courses/backend/node/week2/session-materials/phonebook/phonebook.js @@ -4,34 +4,35 @@ import knex from "./database.js"; // Create const createContact = async (name, phonenumber) => { - const [id] = await knex('contacts').insert({ name, phonenumber }); + const [id] = await knex("contacts").insert({ name, phonenumber }); return id; }; // Read (all) const getContacts = async () => { - return await knex('contacts').select('*'); + return await knex("contacts").select("*"); }; // Read (by id) const getContactById = async (id) => { - return await knex('contacts').where({ id }).first(); + return await knex("contacts").where({ id }).first(); }; // Update const updateContact = async (id, updates) => { - await knex('contacts').where({ id }).update(updates); + await knex("contacts").where({ id }).update(updates); return await getContactById(id); // return updated record -};b +}; +b; // Delete const deleteUser = async (id) => { - await knex('contacts').where({ id }).del(); + await knex("contacts").where({ id }).del(); return { deleted: true }; }; const main = async () => { // Call your functions here to test them -} +}; main().catch(console.error); diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 6428e379..18e71d96 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -8,13 +8,13 @@ ## Database interaction with Knex -Trainees have used Knex before. In foundation, they used it with the .raw() command to execute SQL easily. And they also used it last week when learning about Express. +Trainees have used Knex before. In foundation, they used it with the .raw() command to execute SQL easily. And they also used it last week when learning about Express. This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. 1. Explain what Knex is in more detail, and why we are using it here 2. Compare using .raw() to the Query Builder methods -(TODO write some more content for these two bullet points to guide the mentor) + (TODO write some more content for these two bullet points to guide the mentor) ### Live coding @@ -45,19 +45,21 @@ Note: These exericses could be demod by the mentor, or left to the trainees to d Collections let you group related requests into a reusable library. This makes it easy to organize, run, and share sets of API calls. Read more on the [Official docs](https://learning.postman.com/docs/collections/collections-overview/). -#### Exercise +#### Exercise 1 + Create a collection for your Snippets API. Add an unauthenticated `GET /api/snippets` request, and save it to the collection. -1. In Postman, click **New → Collection**, and give it a meaningful name and description. -2. On the collection, click the **+** icon and create the GET request. Give it a meaningful name and **Save** it to the collection, once it's working. +1. In Postman, click **New → Collection**, and give it a meaningful name and description. +2. On the collection, click the **+** icon and create the GET request. Give it a meaningful name and **Save** it to the collection, once it's working. ### 2. Set Up Multiple Environments -Environments in Postman let you define sets of variables (e.g. base URLs, tokens) for different contexts. That could be your local environment, staging, and production. Switching environments changes the variable values used in your requests. +Environments in Postman let you define sets of variables (e.g. base URLs, tokens) for different contexts. That could be your local environment, staging, and production. Switching environments changes the variable values used in your requests. Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/variables/managing-environments/). -#### Exercise -1. In Postman, go to **Environments** (top-right environment selector) and click **Add**. +#### Exercise 2 + +1. In Postman, go to **Environments** (top-right environment selector) and click **Add**. 2. Name your environment your first environment `Local` 3. Add a variable called `base_url` and set it to `http://localhost:3000` (or wherver you're running your local server). 4. Update your requests to use them in the URL, like: `{{base_url}}/api/snippets`. @@ -71,13 +73,14 @@ In the future, when you come to deploy your app to the web, you can create a new You'll often need to use sensitive data in your requests, namely secrets (API keys, passwords, tokens). These should not be hard-coded in your requests for security reasons! Postman provides a **Vault** and sensitive variable settings to securely store and reuse secrets. Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/postman-vault/postman-vault-secrets/). -#### Exercise +#### Exercise 3 + Let's add the authenticated request to `GET /api/snippets`, so we can test returning private snippets. -1. Open the **Postman Vault** (Vault icon or `Ctrl+Shift+V`). +1. Open the **Postman Vault** (Vault icon or `Ctrl+Shift+V`). 2. When prompted, Postman generates a vault key - save it securely. 3. Add a new secret called `users_token` with a value from the `users.token` column in your database. -4. Now you're ready to use it in a request! Create a new GET request as you did in the first exercise, but this time add an authorization header. Where you need to reference your token, use `{{vault:users_token}}`. +4. Now you're ready to use it in a request! Create a new GET request as you did in the first exercise, but this time add an authorization header. Where you need to reference your token, use `{{vault:users_token}}`. Now you can safely and securely test APIs using secrets. Test to make sure the request is working correctly, and save it to your collection. @@ -85,18 +88,21 @@ Now you can safely and securely test APIs using secrets. Test to make sure the r Postman allows you to write test scripts (in JavaScript) that validate your API responses — checking status codes, payloads, and performance. These tests can be grouped into collections and run automatically. They are a handy way to validate that your API is working correctly, and continues to work correctly as you make changes. Read more on the [Official docs](https://learning.postman.com/docs/tests-and-scripts/write-scripts/test-scripts/). -#### Exercise -1. In a request, open the **Tests** tab. +#### Exercise 4 + +1. In a request, open the **Tests** tab. 2. Write assertions using `pm.test()` and the `pm.response` object. Here are two examples you can paste in: + ```javascript pm.test("Status code is 200", function () { pm.response.to.have.status(200); }); pm.test("Snippet has a numeric 'id' field", function () { - const json = pm.response.json(); - pm.expect(json.id).to.be.a("number"); + const json = pm.response.json(); + pm.expect(json.id).to.be.a("number"); }); ``` + 3. Write at least one additional test case for both of your requests. 4. These tests will run automatically each time you send the request. You can also run them all together, like a test suite. Click on your collection, and find the "Run" button. Make sure all the requests are checked that you wish to test. Click run, and a report will display all of test results. From f34e3ae67e9bd46ad3159996e21787fc782090f1 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 4 Nov 2025 14:51:09 +0100 Subject: [PATCH 15/32] Added more info to explain knex --- courses/backend/node/week2/session-plan.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 18e71d96..2b62daec 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -13,8 +13,16 @@ Trainees have used Knex before. In foundation, they used it with the .raw() comm This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. 1. Explain what Knex is in more detail, and why we are using it here + - Knex is SQL query builder for Node, which helps us write databases queries more cleanly, without raw SQL. + - It supports multiple databases (e.g. SQLite, Postgres) with the same API. + - It helps avoid security issues like SQL injection by handling things safely for us. + - It provides advanced features such as migrations. + 2. Compare using .raw() to the Query Builder methods - (TODO write some more content for these two bullet points to guide the mentor) + - `.raw()`, which you've been using so far, lets you run plain SQL + - However, you lose the "single API" benefit of using Knex + - You also lose the safety of Knex's security precautions regarding SQL injetion + - Using the Query Builder methods like `.select()` help us write safer, more readable DB queries that work even if we change our database type. ### Live coding From 7c0cefe4e3f2f7fb50f481bfd370e8a949e984a8 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 11:07:57 +0100 Subject: [PATCH 16/32] Apply suggestion from @magdazelena MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Magdalena Odrowąż-Żelezik --- courses/backend/node/week2/session-plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 2b62daec..964b8c0c 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -8,7 +8,7 @@ ## Database interaction with Knex -Trainees have used Knex before. In foundation, they used it with the .raw() command to execute SQL easily. And they also used it last week when learning about Express. +Trainees have used Knex before. In foundation, they used it with the `.raw()` command to execute SQL easily. And they also used it last week when learning about Express. This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. From aeaa73a935432d7a69b1bf8d00b841441a82d526 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 11:28:19 +0100 Subject: [PATCH 17/32] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Magdalena Odrowąż-Żelezik --- courses/backend/node/week2/preparation.md | 4 ---- courses/backend/node/week2/session-plan.md | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/courses/backend/node/week2/preparation.md b/courses/backend/node/week2/preparation.md index 0a2c6f55..806d166a 100644 --- a/courses/backend/node/week2/preparation.md +++ b/courses/backend/node/week2/preparation.md @@ -4,8 +4,4 @@ - up until the `The Visual Studio Code REST client` section (15 min) - - Free API for testing and prototyping. (5 min) -## Flipped classroom videos -- [Connecting nodejs to a database using Knex part 1 - Class 3](https://youtu.be/W5xFbiAl4bo) -- [Connecting nodejs to a database using Knex part 2 - Class 3](https://youtu.be/cacTSGU7Hrc) -- [Creating an api using nodejs and express - Class 3](https://youtu.be/i-BUdUMz6Zk) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 964b8c0c..77fd9894 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -12,13 +12,13 @@ Trainees have used Knex before. In foundation, they used it with the `.raw()` co This part of the module should explain Knex in a lot more technical detail. The following implementation is how we expect to see trainees using database interaction from this point forward. -1. Explain what Knex is in more detail, and why we are using it here +1. What is Knex, and why we need it - Knex is SQL query builder for Node, which helps us write databases queries more cleanly, without raw SQL. - It supports multiple databases (e.g. SQLite, Postgres) with the same API. - It helps avoid security issues like SQL injection by handling things safely for us. - It provides advanced features such as migrations. -2. Compare using .raw() to the Query Builder methods +2. Comparison: `.raw()` method vs. the Query Builder methods - `.raw()`, which you've been using so far, lets you run plain SQL - However, you lose the "single API" benefit of using Knex - You also lose the safety of Knex's security precautions regarding SQL injetion From db336e0028ccff19beeb1d9afdff9b00e2553755 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 12:06:15 +0100 Subject: [PATCH 18/32] Remove meal sharing tasks and move knex cheatsheet to prep --- courses/backend/node/week2/assignment.md | 54 ----------------------- courses/backend/node/week2/preparation.md | 1 + 2 files changed, 1 insertion(+), 54 deletions(-) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index d3188c42..8fa014ee 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -147,57 +147,3 @@ You can for instance demonstrate this with a screen recording and include it in After having demonstrated the SQL injection vulnerability, the goal is then to fix the issue by updating `app.js`. **Hint:** the `multipleStatements: true` part in the configuration indicates how you can use the vulnerability. The configuration should not be changed though, the SQL injection should be fixed by making changes in the `/api/contacts` route. - -## Meal sharing endpoints - -You will continue working in the meal-sharing repository for this task. - -You should have the basic [CRUD](https://www.freecodecamp.org/news/crud-operations-explained/) endpoints for **meals** and **reservations** as the result of last week's homework. This week, you will add **query parameters**, that will allow you to **sort** and **filter** the information retrieved from the database. - -### Routes - -#### Meals - -Work with your `GET api/meals` route to add the query parameters. -Make sure that the query parameters can be combined, f.x. `?limit=4&maxPrice=90`. - -| Parameter | Data type | Description | Example | -| ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| `maxPrice` | Number | Returns all meals that are cheaper than `maxPrice`. | `api/meals?maxPrice=90` | -| `availableReservations` | Boolean | Returns all meals that still have available spots left, if `true`. If `false`, return meals that have no available spots left.[^1] | `api/meals?availableReservations=true` | -| `title` | String | Returns all meals that partially match the given title. `Rød grød` will match the meal with the title `Rød grød med fløde`. | `api/meals?title=Indian%20platter` | -| `dateAfter` | Date | Returns all meals where the date for `when` is after the given date. | `api/meals?dateAfter=2022-10-01` | -| `dateBefore` | Date | Returns all meals where the date for `when` is before the given date. | `api/meals?dateBefore=2022-08-08` | -| `limit` | Number | Returns the given number of meals. | `api/meals?limit=7` | -| `sortKey`[^2] | String | Returns all meals sorted by the given key. Allows `when`, `max_reservations` and `price` as keys. Default sorting order is asc(ending). | `api/meals?sortKey=price` | -| `sortDir`[^3] | String | Returns all meals sorted in the given direction. Only works combined with the `sortKey` and allows `asc` or `desc`. | `api/meals?sortKey=price&sortDir=desc` | - -[^1]: `availableReservations` requires you to work with several database tables at once. Try practicing the right query in MySQL Workbench first (you might have it from Database week2 homework) and once you have it working, build it with `knex`. - -[^2]: This used to be `sort_key` in a previous version of the homework text. - -[^3]: This used to be `sort_dir` in a previous version of the homework text. - -#### Reviews - -By now, you have the basic set of endpoints for **meals** and **reservations** and even a collection of query parameters for **meals**. To practice a bit more and finalize the basic backend functionality, create the set of routes for **reviews**: - -| Route | HTTP method | Description | -| ----------------------------- | ----------- | ---------------------------------------- | -| `/api/reviews` | GET | Returns all reviews. | -| `/api/meals/:meal_id/reviews` | GET | Returns all reviews for a specific meal. | -| `/api/reviews` | POST | Adds a new review to the database. | -| `/api/reviews/:id` | GET | Returns a review by `id`. | -| `/api/reviews/:id` | PUT | Updates the review by `id`. | -| `/api/reviews/:id` | DELETE | Deletes the review by `id`. | - -#### Knex - -You should try to avoid using `knex.raw` and instead use the different `knex` functions, for example: - -- `.select`, `.from`, `.where`, `join`, `leftJoin` -- `.insert` -- `.update` -- `.del` (for deletion) - -Check out the [Knex cheatsheet](https://devhints.io/knex)! diff --git a/courses/backend/node/week2/preparation.md b/courses/backend/node/week2/preparation.md index 806d166a..226d0e7a 100644 --- a/courses/backend/node/week2/preparation.md +++ b/courses/backend/node/week2/preparation.md @@ -2,6 +2,7 @@ - [NodeJS Web API with KNEX and Express](https://www.youtube.com/watch?v=QNw9q4YXR4E) (15 min) - up until the `The Visual Studio Code REST client` section (15 min) +- Familiarise yourself with the [Knex Cheatsheet](https://devhints.io/knex) (especially Select, Scheme and Modifying sections) - - Free API for testing and prototyping. (5 min) From 776f142e28293825636678290376a3055b4ea587 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 14:38:12 +0100 Subject: [PATCH 19/32] simplify assignment --- courses/backend/node/week2/assignment.md | 79 ++++++------------------ 1 file changed, 19 insertions(+), 60 deletions(-) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index 8fa014ee..42b23f77 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -1,51 +1,31 @@ # Assignment -Once again, you will deliver 2 pull requests: +TODO - We should add a task to practice postman too. Maybe add all their final endpoints to their collection. And anything else in the session that we haven't covered here yet! -- A pull request for the **Warmup** - in your regular hyf-homework repository -- A pull request for the additional **meal sharing endpoints** - in the meal-sharing repository +## Task 1 -In both repositories, create a `nodejs-week2` branch from `main` to work on the homework (`git checkout -b nodejs-week2` ). +You'll set up and work with your own version of the Contacts API that you saw in the session. -TODO - We should add a task to practice postman too. Maybe add all their final endpoints to their collection. - -## Warmup - -For the warmup you will be handed a Contacts API with a single endpoint: +It will have one endpoint: - `GET /api/contacts` -This endpoint accepts a query parameter `sort`. Here's how you can use it: +This endpoint accepts a query parameter `sort`. Here's how it should be possible to use it: - `GET /api/contacts?sort=first_name%20ASC` - Sorts contacts by first name, ascending - `GET /api/contacts?sort=last_name%20DESC` - Sorts contacts by last name, descending -But this `sort` query parameter has been introduced with a SQL injection vulnerability and the goal is to demonstrate the issue and then fix and remove the vulnerability. +But this `sort` query parameter will introduce a security issue, an SQL injection will be possible. The goal is to demonstrate the issue and then fix it to remove the vulnerability. ### Setup TODO - Review assignment to work with sqlite. -Go to `nodejs/week2` in your `hyf-homework` repo: - -```shell -npm init -y -npm i express mysql2 knex -npm i --save-dev nodemon -npm set-script dev "nodemon app.js" -``` - -Make sure you have `"type": "module"` in your `package.json`. - -You should also ensure that the `node_modules/` folder is ignored by Git: - -```shell -echo node_modules/ >> .gitignore -``` - -Create a database/schema called `hyf_node_week2_warmup` with a `contacts` table: +1. Go to/create a `node/week2` directory in your `hyf-assignment` repo. +2. Create yourself a new node application +3. Create a database called `phonebook` with a `contacts` table, with the following schema and data: ```sql CREATE TABLE `contacts` ( @@ -85,34 +65,18 @@ insert into contacts (id, first_name, last_name, email, phone) values (24, 'Raqu insert into contacts (id, first_name, last_name, email, phone) values (25, 'Tabor', 'Reavey', null, null); ``` -Create `app.js`: +4. Set up Express and an Sqlite connection in your node application. In your knex instance, make sure to set: `multipleStatements: true` - this is important! -```js -import knex from "knex"; -const knexInstance = knex({ - client: "mysql2", - connection: { - host: process.env.DB_HOST || "127.0.0.1", - port: process.env.DB_PORT || 3306, - user: process.env.DB_USER || "root", - password: process.env.DB_PASSWORD || "my-secret-pw", - database: process.env.DB_NAME || "hyf_node_week2_warmup", - multipleStatements: true, - }, -}); - -import express from "express"; -const app = express(); -const port = process.env.PORT || 3000; +5. Make sure you have an API router under the `/api` path set up like so: -app.use(express.json()); - -const apiRouter = express.Router(); +```js app.use("/api", apiRouter); +``` -const contactsAPIRouter = express.Router(); -apiRouter.use("/contacts", contactsAPIRouter); +6. Create a contacts router at `/contacts`, and attach it to your API router. +7. In your contacts API, create the following endpoint: +```js contactsAPIRouter.get("/", async (req, res) => { let query = knexInstance.select("*").from("contacts"); @@ -133,17 +97,12 @@ contactsAPIRouter.get("/", async (req, res) => { res.status(500).json({ error: "Internal server error" }); } }); - -app.listen(port, () => { - console.log(`Listening on port ${port}`); -}); ``` As mentioned above, the `sort` query parameter has been introduced with a SQL injection vulnerability. -First, you should demonstrate the SQL injection and that it for instance is possible to drop/delete the `contacts` table with the `sort` query parameter. -You can for instance demonstrate this with a screen recording and include it in the PR description. +First, you should demonstrate the SQL injection and that, for instance, it is possible to drop/delete the `contacts` table with the `sort` query parameter. Capture this demonstration with a screen recording, and attach it to your PR when you submit your assignment. -After having demonstrated the SQL injection vulnerability, the goal is then to fix the issue by updating `app.js`. +After having demonstrated the SQL injection vulnerability, your task is then to fix the issue by updating `app.js`. -**Hint:** the `multipleStatements: true` part in the configuration indicates how you can use the vulnerability. The configuration should not be changed though, the SQL injection should be fixed by making changes in the `/api/contacts` route. +**Hint:** the `multipleStatements: true` part in the configuration indicates how you can use the vulnerability. The configuration should not be changed though, the SQL injection should be fixed by making changes in the `/api/contacts` route only. From bf506e2b361bba55528943fcbaa1991fa97447dc Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 14:38:26 +0100 Subject: [PATCH 20/32] remove sqlite todo --- courses/backend/node/week2/assignment.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index 42b23f77..a6ef5409 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -21,8 +21,6 @@ But this `sort` query parameter will introduce a security issue, an SQL injectio ### Setup -TODO - Review assignment to work with sqlite. - 1. Go to/create a `node/week2` directory in your `hyf-assignment` repo. 2. Create yourself a new node application 3. Create a database called `phonebook` with a `contacts` table, with the following schema and data: From 95a0c572f45bd2db0034a9d5f4e087104777bbc6 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 14:43:57 +0100 Subject: [PATCH 21/32] simplifieid live coding instructions --- courses/backend/node/week2/session-plan.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 77fd9894..e1714edf 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -26,12 +26,10 @@ This part of the module should explain Knex in a lot more technical detail. The ### Live coding -Run through the [phonebook example](./session-materials/phonebook/). The code is already written, but feel free to clear some of the functions so you can write them together in the session. +Run through the [phonebook example](./session-materials/phonebook/). The functions are already written, but feel free to clear them and write them together in the session. -1. Walk through `database.js` and explain the setup -2. Create the database following the instructions in `database.js` -3. Run through `phonebook.js` function by function, pointing to the documentation which explains all the query builder methods. -4. After writing each function, test it by running it via the main function. +1. Set up [`database.js`](./session-materials/phonebook/database.js) +2. Walk through [`phonebook.js`](./session-materials/phonebook/phonebook.js) ## Snippets API continued From 9acfe5b604b4a43b7fcf55f6758360adb12463b2 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 14:44:31 +0100 Subject: [PATCH 22/32] removed redundent instruciton --- courses/backend/node/week2/session-plan.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index e1714edf..163a10a3 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -45,8 +45,6 @@ TODO: Add a section with live coding somewhere below to explain error handling? Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. -Note: These exericses could be demod by the mentor, or left to the trainees to do on their own machines. Or a mix of both, whatever feels natural. - ### 1. Creating collections and saving requests Collections let you group related requests into a reusable library. This makes it easy to organize, run, and share sets of API calls. Read more on the [Official docs](https://learning.postman.com/docs/collections/collections-overview/). From 00b22d5f9998aec48bfffab6d98b3abceaa53467 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 15:35:13 +0100 Subject: [PATCH 23/32] expand on the assignment to mirror more of the sessions learning goals --- courses/backend/node/week2/assignment.md | 47 ++++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index a6ef5409..6abe8565 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -1,12 +1,8 @@ # Assignment -TODO - We should add a task to practice postman too. Maybe add all their final endpoints to their collection. And anything else in the session that we haven't covered here yet! +You'll set up and work with your own version of a simple Contacts API. -## Task 1 - -You'll set up and work with your own version of the Contacts API that you saw in the session. - -It will have one endpoint: +It will start with one endpoint (and you will add more throughout the task): - `GET /api/contacts` @@ -17,9 +13,7 @@ This endpoint accepts a query parameter `sort`. Here's how it should be possible - `GET /api/contacts?sort=last_name%20DESC` - Sorts contacts by last name, descending -But this `sort` query parameter will introduce a security issue, an SQL injection will be possible. The goal is to demonstrate the issue and then fix it to remove the vulnerability. - -### Setup +## Setup 1. Go to/create a `node/week2` directory in your `hyf-assignment` repo. 2. Create yourself a new node application @@ -97,10 +91,39 @@ contactsAPIRouter.get("/", async (req, res) => { }); ``` -As mentioned above, the `sort` query parameter has been introduced with a SQL injection vulnerability. + +## The tasks + +### Task 1 - Solve the SQL injection + +The current implementation of the `sort` query parameter has introduced an SQL injection vulnerability. First, you should demonstrate the SQL injection and that, for instance, it is possible to drop/delete the `contacts` table with the `sort` query parameter. Capture this demonstration with a screen recording, and attach it to your PR when you submit your assignment. -After having demonstrated the SQL injection vulnerability, your task is then to fix the issue by updating `app.js`. +After having demonstrated the SQL injection vulnerability, your task is then to fix the issue. Your solution should be solved in the `app.js` file only. While the the `multipleStatements: true` configuration you used enables this vulnerability, it should not be changed in your solution. + +### Task 2 - Improve your API + +Create two additional endpoints to enable the following functionality: +1. Create new contacts +2. Delete an existing contact + +### Task 3 - Error handling + +Update your endpoints with appropriate error handling. You should, at least, handle the following cases: +1. Successful requests +2. Incorrect requests (e.g. an incorrectly formatted sort request) +3. Server issues (e.g. a missing database table, or an offline database) +4. A catch all for any other errors + +Remember to: +1. Return the appropriate HTTP code +2. Avoid sending any implementation or internal data to the client +3. Log an appropriate message so you can debug issues that occur in your service + +### Task 3 - Postman + +1. Create a Postman collection to capture some example requests with your new API. +2. Create a basic test suite that you can run to validate that everything is working correctly. -**Hint:** the `multipleStatements: true` part in the configuration indicates how you can use the vulnerability. The configuration should not be changed though, the SQL injection should be fixed by making changes in the `/api/contacts` route only. +Share both a link to your Collection and a link to a test run showing your tests passing in your pull request. From 5b805a17bb8b37ff262f6e201224647dc607c39b Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 15:39:50 +0100 Subject: [PATCH 24/32] linting fixes --- courses/backend/node/week2/assignment.md | 4 +++- courses/backend/node/week2/preparation.md | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/courses/backend/node/week2/assignment.md b/courses/backend/node/week2/assignment.md index 6abe8565..50146331 100644 --- a/courses/backend/node/week2/assignment.md +++ b/courses/backend/node/week2/assignment.md @@ -91,7 +91,6 @@ contactsAPIRouter.get("/", async (req, res) => { }); ``` - ## The tasks ### Task 1 - Solve the SQL injection @@ -105,18 +104,21 @@ After having demonstrated the SQL injection vulnerability, your task is then to ### Task 2 - Improve your API Create two additional endpoints to enable the following functionality: + 1. Create new contacts 2. Delete an existing contact ### Task 3 - Error handling Update your endpoints with appropriate error handling. You should, at least, handle the following cases: + 1. Successful requests 2. Incorrect requests (e.g. an incorrectly formatted sort request) 3. Server issues (e.g. a missing database table, or an offline database) 4. A catch all for any other errors Remember to: + 1. Return the appropriate HTTP code 2. Avoid sending any implementation or internal data to the client 3. Log an appropriate message so you can debug issues that occur in your service diff --git a/courses/backend/node/week2/preparation.md b/courses/backend/node/week2/preparation.md index 226d0e7a..95271fcd 100644 --- a/courses/backend/node/week2/preparation.md +++ b/courses/backend/node/week2/preparation.md @@ -4,5 +4,3 @@ - up until the `The Visual Studio Code REST client` section (15 min) - Familiarise yourself with the [Knex Cheatsheet](https://devhints.io/knex) (especially Select, Scheme and Modifying sections) - - Free API for testing and prototyping. (5 min) - - From 42a6daf670bff3c8e5b57f5e094bce2ed21c941f Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Mon, 10 Nov 2025 15:46:25 +0100 Subject: [PATCH 25/32] updated session plan wording to be a little less mixed between mentor and trainee --- courses/backend/node/week2/session-plan.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 163a10a3..15f412a1 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -43,22 +43,25 @@ TODO: Add a section with live coding somewhere below to explain error handling? ## Advanced Postman -Postman can be used for quickly testing your APIs, but can also be configured in more advanced ways to support your development workflow. Here, you'll learn about four ways to level up your Postman game. +Postman can be used for quickly testing APIs, but it can also be configured in more advanced ways to support the development workflow. Here are four ways trainees can level up their Postman game. ### 1. Creating collections and saving requests -Collections let you group related requests into a reusable library. This makes it easy to organize, run, and share sets of API calls. Read more on the [Official docs](https://learning.postman.com/docs/collections/collections-overview/). +Collections let you group related requests into a reusable library. This makes it easy to organize, run, and share sets of API calls. + +Read more on the [Official docs](https://learning.postman.com/docs/collections/collections-overview/). #### Exercise 1 -Create a collection for your Snippets API. Add an unauthenticated `GET /api/snippets` request, and save it to the collection. +Create a collection for the Snippets API. Add an unauthenticated `GET /api/snippets` request, and save it to the collection. 1. In Postman, click **New → Collection**, and give it a meaningful name and description. 2. On the collection, click the **+** icon and create the GET request. Give it a meaningful name and **Save** it to the collection, once it's working. ### 2. Set Up Multiple Environments -Environments in Postman let you define sets of variables (e.g. base URLs, tokens) for different contexts. That could be your local environment, staging, and production. Switching environments changes the variable values used in your requests. +Environments in Postman let you define sets of variables (e.g. base URLs, tokens) for different contexts. That could be a local environment, staging, and production. Switching environments changes the variable values used in your requests. + Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/variables/managing-environments/). #### Exercise 2 @@ -75,7 +78,9 @@ In the future, when you come to deploy your app to the web, you can create a new ### 3. Managing Secrets -You'll often need to use sensitive data in your requests, namely secrets (API keys, passwords, tokens). These should not be hard-coded in your requests for security reasons! Postman provides a **Vault** and sensitive variable settings to securely store and reuse secrets. Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/postman-vault/postman-vault-secrets/). +You'll often need to use sensitive data in requests, namely secrets (API keys, passwords, tokens). These should not be hard-coded in the requests for security reasons! Postman provides a **Vault** and sensitive variable settings to securely store and reuse secrets. + +Read more on the [Official docs](https://learning.postman.com/docs/sending-requests/postman-vault/postman-vault-secrets/). #### Exercise 3 @@ -90,7 +95,9 @@ Now you can safely and securely test APIs using secrets. Test to make sure the r ### 4. Create Basic Test Suites -Postman allows you to write test scripts (in JavaScript) that validate your API responses — checking status codes, payloads, and performance. These tests can be grouped into collections and run automatically. They are a handy way to validate that your API is working correctly, and continues to work correctly as you make changes. Read more on the [Official docs](https://learning.postman.com/docs/tests-and-scripts/write-scripts/test-scripts/). +Postman allows you to write test scripts (in JavaScript) that validate your API responses — checking status codes, payloads, and performance. These tests can be grouped into collections and run automatically. They are a handy way to validate that your API is working correctly, and continues to work correctly as changes are made. + +Read more on the [Official docs](https://learning.postman.com/docs/tests-and-scripts/write-scripts/test-scripts/). #### Exercise 4 From 2ba2dfab783b71ea1da0c6606e15389033de85db Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 11 Nov 2025 13:47:41 +0100 Subject: [PATCH 26/32] added initial content for error handling --- .../week2/session-materials/phonebook/api.js | 18 ++++++++ .../phonebook/api/contacts.js | 18 ++++++++ courses/backend/node/week2/session-plan.md | 44 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 courses/backend/node/week2/session-materials/phonebook/api.js create mode 100644 courses/backend/node/week2/session-materials/phonebook/api/contacts.js diff --git a/courses/backend/node/week2/session-materials/phonebook/api.js b/courses/backend/node/week2/session-materials/phonebook/api.js new file mode 100644 index 00000000..03006893 --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/api.js @@ -0,0 +1,18 @@ +import express from "express"; +import contactsRouter from "./api/contacts.js"; + +const app = express(); +const router = express.Router(); + +const port = process.env.PORT || 3000; + +// Parse URL-encoded bodies (as sent by HTML forms) +app.use(express.urlencoded({ extended: true })); +// Parse JSON bodies (as sent by API clients) +app.use(express.json()); + +router.use("/contacts", contactsRouter); + +app.use("/api", router); + +app.listen(port, () => console.log(`Server listening on port ${port}!`)); \ No newline at end of file diff --git a/courses/backend/node/week2/session-materials/phonebook/api/contacts.js b/courses/backend/node/week2/session-materials/phonebook/api/contacts.js new file mode 100644 index 00000000..abb14c9c --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/api/contacts.js @@ -0,0 +1,18 @@ +import express from "express"; +import knex from "../database.js"; + +const router = express.Router(); + +// Example POST endpoint to add a new contact +router.post("/", async (request, response) => { + try { + console.log(response.body); // Server side log, for developers + const insertedContact = await knex("contacts").insert(response.body); // This could be insecure! + response.status(201).json(insertedContact); // 201 Created + } catch (error) { + console.error("Error inserting contact:", error); // Server side error, for developers + response.status(500).json({ message: "Something went wrong on the server." }); // Client side error, for users. Avoid leaking database info. + } +}); + +export default router; \ No newline at end of file diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 15f412a1..8a16b056 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -41,6 +41,50 @@ TODO: Add a section with live coding somewhere below to explain error handling? 2. [PUT endpoint exercise](./session-materials/08-put-endpoint.md) 3. [DELETE endpoint exercise](./session-materials/09-delete-endpoint.md) +## Error handling + +Error handling is important so we have visibility of issues that occur in applications, and gain some understanding of what is going wrong. + +### HTTP Status Codes Refresher + +Here are some of the most commonly used: + +#### 2XX - Success +`200 OK` - The request succeeded, e.g. a webpage loads as it should. +`201 Created` - A new resource was made, e.g. a new user account. + +#### 3XX - Redirection +`301 Moved Permanently` - The URL has changed, e.g. redirect from oldsite.com to newsite.com. +`302 Found` - A temporary redirect, e.g. redirecting Spanish visitors to the Spanish version of the website. + +#### 4XX - Client Errors +`400 Bad Request` - The request was invalid, e.g. form data missing or incorrect. +`401 Unauthorized` - You need to log in e.g. trying to access user features when logged out. +`404 Not Found` - Nothing at that URL e.g. a missing page or resource. + +#### 5XX - Client Errors +`500 Internal Server Error` - Generic server issue, e.g. something goes wrong in the backend. +`503 Service Unavailable` - Server is down or busy e.g. backend API is not running. + +Read more at [HTTP Status cheatsheet](https://devhints.io/http-status). + +### Client vs Server + +Server-side errors should be designed for developers. Detailed errors help debugging and ultimately fixing issues easier. +e.g. If a database table is missing, record the missing table name in your logs. + +Client-side errors should be designed for users, including the correct HTTP status code. +e.g. In the missing database table case, simply return a `500 Internal Server Error` and a useful page to the user to explain how to continue. + +It's important to hide specific error details from the user for multiple reasons: +1. Security - Revealing database names and other internal details can give attackers too many clues about your system which can make your app more vulnerable to exploitation. +2. Privacy - Many internal errors can include sensitive data (e.g. user IDs, personal information) that shouldn't be exposed. +3. User Experience - Some technical errors would confuse most users, so stick with simple, friendly messages that can help the user continue. + +### Live coding + +Walk through [`api/contacts.js`](./session-materials/phonebook/api/contacts.js) to explain the try/catch pattern, appropriate server and client side error handling, correct usage of HTTP codes and why the knex code is insecure. + ## Advanced Postman Postman can be used for quickly testing APIs, but it can also be configured in more advanced ways to support the development workflow. Here are four ways trainees can level up their Postman game. From 0738758118273933a3eea98dd8f0fe5cdf81358e Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 11 Nov 2025 13:54:06 +0100 Subject: [PATCH 27/32] Added package.json etc. for live coding materials --- .../week2/session-materials/phonebook/api.js | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 courses/backend/node/week2/session-materials/phonebook/api.js diff --git a/courses/backend/node/week2/session-materials/phonebook/api.js b/courses/backend/node/week2/session-materials/phonebook/api.js deleted file mode 100644 index 03006893..00000000 --- a/courses/backend/node/week2/session-materials/phonebook/api.js +++ /dev/null @@ -1,18 +0,0 @@ -import express from "express"; -import contactsRouter from "./api/contacts.js"; - -const app = express(); -const router = express.Router(); - -const port = process.env.PORT || 3000; - -// Parse URL-encoded bodies (as sent by HTML forms) -app.use(express.urlencoded({ extended: true })); -// Parse JSON bodies (as sent by API clients) -app.use(express.json()); - -router.use("/contacts", contactsRouter); - -app.use("/api", router); - -app.listen(port, () => console.log(`Server listening on port ${port}!`)); \ No newline at end of file From b9e63e68cfa89377dec837be3a3fe012f13f967c Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 11 Nov 2025 14:00:15 +0100 Subject: [PATCH 28/32] Actually include package.json etc. in this commit --- .../session-materials/phonebook/index.js | 18 + .../phonebook/package-lock.json | 2230 +++++++++++++++++ .../session-materials/phonebook/package.json | 18 + 3 files changed, 2266 insertions(+) create mode 100644 courses/backend/node/week2/session-materials/phonebook/index.js create mode 100644 courses/backend/node/week2/session-materials/phonebook/package-lock.json create mode 100644 courses/backend/node/week2/session-materials/phonebook/package.json diff --git a/courses/backend/node/week2/session-materials/phonebook/index.js b/courses/backend/node/week2/session-materials/phonebook/index.js new file mode 100644 index 00000000..03006893 --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/index.js @@ -0,0 +1,18 @@ +import express from "express"; +import contactsRouter from "./api/contacts.js"; + +const app = express(); +const router = express.Router(); + +const port = process.env.PORT || 3000; + +// Parse URL-encoded bodies (as sent by HTML forms) +app.use(express.urlencoded({ extended: true })); +// Parse JSON bodies (as sent by API clients) +app.use(express.json()); + +router.use("/contacts", contactsRouter); + +app.use("/api", router); + +app.listen(port, () => console.log(`Server listening on port ${port}!`)); \ No newline at end of file diff --git a/courses/backend/node/week2/session-materials/phonebook/package-lock.json b/courses/backend/node/week2/session-materials/phonebook/package-lock.json new file mode 100644 index 00000000..52ed63d2 --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/package-lock.json @@ -0,0 +1,2230 @@ +{ + "name": "phonebook", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "phonebook", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^5.1.0", + "knex": "^3.1.0", + "sqlite3": "^5.1.7" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.80.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz", + "integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/courses/backend/node/week2/session-materials/phonebook/package.json b/courses/backend/node/week2/session-materials/phonebook/package.json new file mode 100644 index 00000000..e66d3250 --- /dev/null +++ b/courses/backend/node/week2/session-materials/phonebook/package.json @@ -0,0 +1,18 @@ +{ + "name": "phonebook", + "version": "1.0.0", + "description": "Live coding example", + "author": "", + "license": "ISC", + "type": "module", + "main": "index.js", + + "scripts": { + "dev": "node --watch index.js" + }, + "dependencies": { + "express": "^5.1.0", + "knex": "^3.1.0", + "sqlite3": "^5.1.7" + } +} From 6bed336f26e6d88d22961723a45631e30ec88845 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 11 Nov 2025 14:00:25 +0100 Subject: [PATCH 29/32] Move rest content from readme to session plan --- courses/backend/node/week2/README.md | 20 +---------------- courses/backend/node/week2/session-plan.md | 25 +++++++++++++++++++--- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/courses/backend/node/week2/README.md b/courses/backend/node/week2/README.md index 1cae7746..5b32195b 100644 --- a/courses/backend/node/week2/README.md +++ b/courses/backend/node/week2/README.md @@ -24,22 +24,4 @@ By the end of this session, you will be able to: - [ ] Set up multiple environments - [ ] Managing secrets - [ ] Create basic test suites - -### 1. What is Representational State Transfer (REST)? - -Building software is like building houses: architecture is everything. The design of each part is just as important as the utility of it. REST is a specific architectural style for web applications. It serves to organise code in **predictable** ways. - -The most important features of REST are: - -- An application has a `frontend` (client) and a `backend` (server). This is called [separation of concerns](https://medium.com/machine-words/separation-of-concerns-1d735b703a60): each section has its specific job to do. The frontend deals with presenting data in a user friendly way, the backend deals with all the logic and data manipulation -- The server is `stateless`, which means that it doesn't store any data about a client session. Whenever a client sends a request to the server, each request from the client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. This makes it possible to handle requests from millions of users. -- Server responses can be temporarily stored on the client (a browser) using a process called `caching`: storing files like images or webpages in the browser to load the next time you enter a website (instead of getting them from the server, which generally takes longer to do). -- Client-server communication is done through `Hypertext Transfer Protocol (HTTP)` (more on that later), which serves as the style (the how) of communication. - -It's important to know about REST because it teaches us how web applications are designed and holds us to a standard that makes development and usage predictable. However, don't worry if you don't know what any of this means just yet. It's good to be exposed to it, and understanding will come with experience. - -For more research, check the following resource: - -- [What is REST: a simple explanation for beginners](https://medium.com/extend/what-is-rest-a-simple-explanation-for-beginners-part-1-introduction-b4a072f8740f) - -- [@NoerGitKat (lots of web app clones/examples to learn from)](https://github.com/NoerGitKat) + \ No newline at end of file diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 8a16b056..4bb5cfb6 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -31,11 +31,30 @@ Run through the [phonebook example](./session-materials/phonebook/). The functio 1. Set up [`database.js`](./session-materials/phonebook/database.js) 2. Walk through [`phonebook.js`](./session-materials/phonebook/phonebook.js) -## Snippets API continued +## Implementing REST with Express -Now we can pick up where we left the exercises last week. Help the trainees complete the remaining endpoints: +### REST refresher + +Building software is like building houses: architecture is everything. The design of each part is just as important as the utility of it. REST is a specific architectural style for web applications. It serves to organise code in **predictable** ways. + +The most important features of REST are: + +- An application has a `frontend` (client) and a `backend` (server). This is called [separation of concerns](https://medium.com/machine-words/separation-of-concerns-1d735b703a60): each section has its specific job to do. The frontend deals with presenting data in a user friendly way, the backend deals with all the logic and data manipulation +- The server is `stateless`, which means that it doesn't store any data about a client session. Whenever a client sends a request to the server, each request from the client to server must contain all of the information necessary to understand the request, and cannot take advantage of any stored context on the server. This makes it possible to handle requests from millions of users. +- Server responses can be temporarily stored on the client (a browser) using a process called `caching`: storing files like images or webpages in the browser to load the next time you enter a website (instead of getting them from the server, which generally takes longer to do). +- Client-server communication is done through `Hypertext Transfer Protocol (HTTP)`, which serves as the style (the how) of communication. + +It's important to know about REST because it teaches us how web applications are designed and holds us to a standard that makes development and usage predictable. -TODO: Add a section with live coding somewhere below to explain error handling? include that in one/all of the exercises? +For further reading, check the following resources: + +- [What is REST: a simple explanation for beginners](https://medium.com/extend/what-is-rest-a-simple-explanation-for-beginners-part-1-introduction-b4a072f8740f) +- [@NoerGitKat (lots of web app clones/examples to learn from)](https://github.com/NoerGitKat) + + +### Snippets API continued + +Now we can pick up where we left the exercises last week. Help the trainees complete the remaining endpoints: 1. [POST endpoint exercise](./session-materials/07-post-endpoint.md) 2. [PUT endpoint exercise](./session-materials/08-put-endpoint.md) From f32ad49018fbb181d6803795bbe21d05b9eeb370 Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 11 Nov 2025 14:03:27 +0100 Subject: [PATCH 30/32] linting fixes --- courses/backend/node/week2/README.md | 1 - .../session-materials/phonebook/api/contacts.js | 6 ++++-- .../week2/session-materials/phonebook/index.js | 2 +- .../week2/session-materials/phonebook/package.json | 1 - courses/backend/node/week2/session-plan.md | 14 +++++++++----- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/courses/backend/node/week2/README.md b/courses/backend/node/week2/README.md index 5b32195b..4c2b706f 100644 --- a/courses/backend/node/week2/README.md +++ b/courses/backend/node/week2/README.md @@ -24,4 +24,3 @@ By the end of this session, you will be able to: - [ ] Set up multiple environments - [ ] Managing secrets - [ ] Create basic test suites - \ No newline at end of file diff --git a/courses/backend/node/week2/session-materials/phonebook/api/contacts.js b/courses/backend/node/week2/session-materials/phonebook/api/contacts.js index abb14c9c..d340d052 100644 --- a/courses/backend/node/week2/session-materials/phonebook/api/contacts.js +++ b/courses/backend/node/week2/session-materials/phonebook/api/contacts.js @@ -11,8 +11,10 @@ router.post("/", async (request, response) => { response.status(201).json(insertedContact); // 201 Created } catch (error) { console.error("Error inserting contact:", error); // Server side error, for developers - response.status(500).json({ message: "Something went wrong on the server." }); // Client side error, for users. Avoid leaking database info. + response + .status(500) + .json({ message: "Something went wrong on the server." }); // Client side error, for users. Avoid leaking database info. } }); -export default router; \ No newline at end of file +export default router; diff --git a/courses/backend/node/week2/session-materials/phonebook/index.js b/courses/backend/node/week2/session-materials/phonebook/index.js index 03006893..4524aa07 100644 --- a/courses/backend/node/week2/session-materials/phonebook/index.js +++ b/courses/backend/node/week2/session-materials/phonebook/index.js @@ -15,4 +15,4 @@ router.use("/contacts", contactsRouter); app.use("/api", router); -app.listen(port, () => console.log(`Server listening on port ${port}!`)); \ No newline at end of file +app.listen(port, () => console.log(`Server listening on port ${port}!`)); diff --git a/courses/backend/node/week2/session-materials/phonebook/package.json b/courses/backend/node/week2/session-materials/phonebook/package.json index e66d3250..e29717f3 100644 --- a/courses/backend/node/week2/session-materials/phonebook/package.json +++ b/courses/backend/node/week2/session-materials/phonebook/package.json @@ -6,7 +6,6 @@ "license": "ISC", "type": "module", "main": "index.js", - "scripts": { "dev": "node --watch index.js" }, diff --git a/courses/backend/node/week2/session-plan.md b/courses/backend/node/week2/session-plan.md index 4bb5cfb6..200a4e0b 100644 --- a/courses/backend/node/week2/session-plan.md +++ b/courses/backend/node/week2/session-plan.md @@ -24,7 +24,7 @@ This part of the module should explain Knex in a lot more technical detail. The - You also lose the safety of Knex's security precautions regarding SQL injetion - Using the Query Builder methods like `.select()` help us write safer, more readable DB queries that work even if we change our database type. -### Live coding +### Live coding - Database interaction Run through the [phonebook example](./session-materials/phonebook/). The functions are already written, but feel free to clear them and write them together in the session. @@ -51,7 +51,6 @@ For further reading, check the following resources: - [What is REST: a simple explanation for beginners](https://medium.com/extend/what-is-rest-a-simple-explanation-for-beginners-part-1-introduction-b4a072f8740f) - [@NoerGitKat (lots of web app clones/examples to learn from)](https://github.com/NoerGitKat) - ### Snippets API continued Now we can pick up where we left the exercises last week. Help the trainees complete the remaining endpoints: @@ -69,19 +68,23 @@ Error handling is important so we have visibility of issues that occur in applic Here are some of the most commonly used: #### 2XX - Success + `200 OK` - The request succeeded, e.g. a webpage loads as it should. `201 Created` - A new resource was made, e.g. a new user account. #### 3XX - Redirection + `301 Moved Permanently` - The URL has changed, e.g. redirect from oldsite.com to newsite.com. `302 Found` - A temporary redirect, e.g. redirecting Spanish visitors to the Spanish version of the website. #### 4XX - Client Errors + `400 Bad Request` - The request was invalid, e.g. form data missing or incorrect. `401 Unauthorized` - You need to log in e.g. trying to access user features when logged out. `404 Not Found` - Nothing at that URL e.g. a missing page or resource. #### 5XX - Client Errors + `500 Internal Server Error` - Generic server issue, e.g. something goes wrong in the backend. `503 Service Unavailable` - Server is down or busy e.g. backend API is not running. @@ -89,18 +92,19 @@ Read more at [HTTP Status cheatsheet](https://devhints.io/http-status). ### Client vs Server -Server-side errors should be designed for developers. Detailed errors help debugging and ultimately fixing issues easier. +Server-side errors should be designed for developers. Detailed errors help debugging and ultimately fixing issues easier. e.g. If a database table is missing, record the missing table name in your logs. -Client-side errors should be designed for users, including the correct HTTP status code. +Client-side errors should be designed for users, including the correct HTTP status code. e.g. In the missing database table case, simply return a `500 Internal Server Error` and a useful page to the user to explain how to continue. It's important to hide specific error details from the user for multiple reasons: + 1. Security - Revealing database names and other internal details can give attackers too many clues about your system which can make your app more vulnerable to exploitation. 2. Privacy - Many internal errors can include sensitive data (e.g. user IDs, personal information) that shouldn't be exposed. 3. User Experience - Some technical errors would confuse most users, so stick with simple, friendly messages that can help the user continue. -### Live coding +### Live coding - Error handling Walk through [`api/contacts.js`](./session-materials/phonebook/api/contacts.js) to explain the try/catch pattern, appropriate server and client side error handling, correct usage of HTTP codes and why the knex code is insecure. From dad8970a4bd19e0d206815e6f9a5fc5c432547fc Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 25 Nov 2025 11:34:00 +0100 Subject: [PATCH 31/32] fixed header order in readme --- courses/backend/node/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/courses/backend/node/README.md b/courses/backend/node/README.md index 1415770e..e182d032 100644 --- a/courses/backend/node/README.md +++ b/courses/backend/node/README.md @@ -4,10 +4,10 @@ This module is part of the Backend specialism and focuses on using Node.js to bu ## Contents -| Week | Topic | Preparation | Session Plan | Assignment | -| ---- | ------------------------ | ------------------------------------- | ----------------------------------- | --------------------------------------- | -| 1. | Express | [Preparation](./week1/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](./week1/session-plan.md) | -| 2. | Database connection; API | [Preparation](./week2/preparation.md) | [Assignment](./week1/assignment.md) | [Session plan](./week2/session-plan.md) | +| Week | Topic | Preparation | Session Plan | Assignment | +| ---- | ------------------------ | ------------------------------------- | ----------------------------------------------------- | ----------------------------------- | +| 1. | Express | [Preparation](./week1/preparation.md) | [Session plan](./week1/session-plan.md) (for mentors) | [Assignment](./week1/assignment.md) | +| 2. | Database connection; API | [Preparation](./week2/preparation.md) | [Session plan](./week2/session-plan.md) (for mentors) | [Assignment](./week1/assignment.md) | ## Module Learning Goals From 49fae843d8f6b17c93bea13378789d877d5e577a Mon Sep 17 00:00:00 2001 From: Adam Blanchard Date: Tue, 25 Nov 2025 13:37:22 +0100 Subject: [PATCH 32/32] Added session materials to gitbook sidebar --- SUMMARY.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/SUMMARY.md b/SUMMARY.md index 2fb2c1eb..af6f1bbb 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -116,10 +116,21 @@ - [Assignment](courses/backend/databases/week2/assignment.md) - [Node](courses/backend/node/README.md) - [Week 1](courses/backend/node/week1/README.md) + - [Session Materials](courses/backend/node/week1/session-materials/) + - [1. Server](courses/backend/node/week1/session-materials/01-server.md) + - [2. Schema](courses/backend/node/week1/session-materials/02-schema.md) + - [3. Routing](courses/backend/node/week1/session-materials/03-routing.md) + - [4. Database connection](courses/backend/node/week1/session-materials/04-database-connection.md) + - [5. GET endpoints](courses/backend/node/week1/session-materials/05-get-endpoints.md) + - [6. Auth](courses/backend/node/week1/session-materials/06-auth.md) - [Preparation](courses/backend/node/week1/preparation.md) - [Session Plan](courses/backend/node/week1/session-plan.md) - [Assignment](courses/backend/node/week1/assignment.md) - [Week 2](courses/backend/node/week2/README.md) + - [Session Materials](courses/backend/node/week2/session-materials/) + - [7. POST endpoints](courses/backend/node/week2/session-materials/07-post-endpoint.md) + - [8. PUT endpoints](courses/backend/node/week2/session-materials/08-put-endpoint.md) + - [9. DELETE endpoints](courses/backend/node/week2/session-materials/09-delete-endpoint.md) - [Preparation](courses/backend/node/week2/preparation.md) - [Session Plan](courses/backend/node/week2/session-plan.md) - [Assignment](courses/backend/node/week2/assignment.md)