Skip to content

Commit

Permalink
Merge pull request #36 from UoaWDCC/feat/express-openapi
Browse files Browse the repository at this point in the history
Feature: Express-OpenAPI and backend/README.md
  • Loading branch information
GBHU753 authored May 22, 2024
2 parents 6362732 + 079ad38 commit 295b4eb
Show file tree
Hide file tree
Showing 9 changed files with 1,878 additions and 6 deletions.
162 changes: 162 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Backend - ASPA

> Last updated 05-2023.
> *Highly recommended* to use `ctrl + shift + v` to display this markdown file nicely in VS Code, if using it.
Let's describe some of the tools you need to contribute to the backend!

### Docker Compose

We're using a MySQL database running in a local Docker container for development.
To create this database on your machine, (assuming you have followed the steps in Getting Started wiki page of downloading WSL2 and Docker Desktop, ) all you have to do is run `docker compose up` in the backend directory, i.e.

```
C:\Projects\aspa-portal-v3\backend>docker compose up
```

This starts a "backend" container in Docker which holds a MySQL database.

### Prisma Migrate

Once you have your MySQL database running, you want to make sure it has the correct "structure" to hold your data. How we do this is by running `npx prisma migrate dev` , i.e.

```
C:\Projects\aspa-portal-v3\backend>npx prisma migrate dev
```

This enforces the current `schema.prisma` file found in the `backend/prisma` directory on the database and generates the project's Prisma Client, which is used to interact with the database in our code.

### Prisma Studio

Prisma Studio is a tool you can use to display the current database state. This is done by running `npx prisma studio` , i.e.

```
C:\Projects\aspa-portal-v3\backend>npx prisma studio
```

This should open the browser and show a page with options to see each table in the database.

### Data population scripts (// TODO: subject to change)

If you've been following this in order, your database might be empty. That's why there are some scripts you can run to populate and delete all the data in your database:

```
// Populate database (use forward slashes if using BASH)
C:\Projects\aspa-portal-v3\backend>node .\tests\helper-scripts\prismaPopulate.js
// Delete all rows from tables
C:\Projects\aspa-portal-v3\backend>node .\tests\helper-scripts\prismaClear.js
```

You can verify the results of these scripts using Prisma Studio.

### OpenAPI

The backend is built in express, but we're not creating just a normal express app. We're using [*express-openapi*](https://www.npmjs.com/package/express-openapi) to build our express app. Long story short, you're gonna have to create all your endpoints in the `backend/api/paths` folder and actually write documentation to accompany it (read the Architecture GitHub wiki page for more info). An example endpoint is shown below, from `backend/api/paths/users.js` :

```
/*
* paths/users.js
* note that the route is defined by the file path and name, i.e., this endpoint is for the /users route.
*/
// to use services (for things like communicating with the database), pass it as a parameter into this function
export default function (usersService) {
let operations = {
GET
};
// this is the GET /users endpoint
async function GET(req, res, next) {
res.status(200).json(await usersService.getAllUsers());
}
// you can define more endpoints for this route here e.g. POST /users, then add it to operations above
// this is documentation you have to define for your endpoints
GET.apiDoc = {
summary: 'Returns all users by id.',
operationId: 'getAllUsers',
parameters: [],
responses: {
200: {
description: 'A list of users that match the requested name.',
schema: {
type: 'array',
items: {
$ref: '#/definitions/User'
}
}
},
default: {
description: 'An error occurred',
schema: {
additionalProperties: true
}
}
}
};
return operations;
}
```

In `backend/app.js` :

```
// this is the important part
initialize({
app,
apiDoc: apiDoc,
// make sure to specify any services you use here
dependencies: {
usersService: usersService,
},
// this takes the endpoints we define, and add them to our app
paths: './api/paths',
docsPath: '/api-docs-json',
});
```

### Prisma Client (// TODO: subject to change)

As mentioned before, running Prisma Migrate also generates the Prisma Client, which is customised to our specific database. The Prisma Client is a tool we import into files that allows us to interact with and perform operations on our database. It will likely be most commonly used to implement function in the services files, e.g. `backend/api/services/usersService.js`

```
/*
* services/usersServices.js
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const usersService = {
async getAllUsers() {
// use prisma client to find all users in the database
const users = await prisma.user.findMany();
return users;
},
};
export default usersService;
```

### Swagger UI

Time to reap the results of our hard work - run `npm run dev` or `yarn run dev` to start up a page that takes our endpoints and displays it to us in an interactive way where we can run them at the click of a button.

```
C:\Projects\aspa-portal-v3\backend>yarn run dev
```

You can use this to check whether you've implemented your endpoint correctly. Also, since the script uses nodemon, you don't have to rerun `npm run dev` every time you update/implement your endpoint - the page should update every time you save :)

### Yarn

Just one last small thing - we're using Yarn as our package manager. That means if you really want to add a package, you should use `yarn add <package>` not `npm install` . If it's devDependency, make sure you run it with the `-D` flag, i.e., `yarn add -D <package` . You can still use npm to run scripts though if you want e.g. `npm run dev` .
119 changes: 119 additions & 0 deletions backend/api/api-doc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
swagger: '2.0'
basePath: '/'
info:
title: 'A getting started API.'
version: '1.0.0'
definitions:
User:
type: 'object'
properties:
id:
type: 'integer'
format: 'int64'
email:
type: 'string'
firstName:
type: 'string'
lastName:
type: 'string'
university:
type: 'string'
studentId:
type: 'string'
upi:
type: 'string'
role:
type: 'string'
skillLevel:
type: 'string'
phoneNumber:
type: 'string'
required:
- 'id'
- 'email'
- 'firstName'
- 'lastName'
- 'role'
- 'skillLevel'
- 'phoneNumber'
Event:
type: 'object'
properties:
id:
type: 'integer'
format: 'int64'
name:
type: 'string'
dateTime:
type: 'string'
format: 'date-time'
venue:
type: 'string'
description:
type: 'string'
price:
type: 'number'
format: 'double'
createdBy:
type: 'string'
attendingExec:
type: 'string'
imageUrl:
type: 'string'
required:
- 'id'
- 'name'
- 'dateTime'
- 'venue'
- 'price'
- 'createdBy'
Ticket:
type: 'object'
properties:
id:
type: 'integer'
format: 'int64'
userId:
type: 'integer'
format: 'int64'
eventId:
type: 'integer'
format: 'int64'
isPaid:
type: 'boolean'
paymentDateTime:
type: 'string'
format: 'date-time'
paymentMethod:
type: 'string'
paymentAmount:
type: 'number'
format: 'double'
required:
- 'id'
- 'userId'
- 'eventId'
- 'isPaid'
- 'paymentDateTime'
- 'paymentMethod'
- 'paymentAmount'
Exec:
type: 'object'
properties:
id:
type: 'integer'
format: 'int64'
name:
type: 'string'
title:
type: 'string'
description:
type: 'string'
imageUrl:
type: 'string'
required:
- 'id'
- 'name'
- 'title'
- 'description'
paths: {}
35 changes: 35 additions & 0 deletions backend/api/paths/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export default function (usersService) {
let operations = {
GET
};

async function GET(req, res, next) {
res.status(200).json(await usersService.getAllUsers());
}

// NOTE: We could also use a YAML string here.
GET.apiDoc = {
summary: 'Returns all users by id.',
operationId: 'getAllUsers',
parameters: [],
responses: {
200: {
description: 'A list of users that match the requested name.',
schema: {
type: 'array',
items: {
$ref: '#/definitions/User'
}
}
},
default: {
description: 'An error occurred',
schema: {
additionalProperties: true
}
}
}
};

return operations;
}
12 changes: 12 additions & 0 deletions backend/api/services/usersService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const usersService = {
async getAllUsers() {
const users = await prisma.user.findMany();
return users;
},
};

export default usersService;
31 changes: 31 additions & 0 deletions backend/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import express from 'express';
import { initialize } from 'express-openapi';
import swaggerUi from 'swagger-ui-express';
import yamljs from 'yamljs';

import usersService from './api/services/usersService.js';


// initialise openapi with express, serving api docs at '/api-docs-json' as json :(
const app = express();
const apiDoc = yamljs.load('./api/api-doc.yml');
initialize({
app,
apiDoc: apiDoc,
dependencies: {
usersService: usersService,
},
paths: './api/paths',
docsPath: '/api-docs-json',
});

// convert from json and serve api docs with a pretty ui using swagger-ui :)
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(null, {
swaggerOptions: {
url: '/api-docs-json',
},
}));

app.listen(3000, () => {
console.log('Server running, API docs available at http://localhost:3000/api-docs');
});
1 change: 0 additions & 1 deletion backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ services:
container_name: aspa-db
image: mysql
restart: always
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: aspadb
Expand Down
Loading

0 comments on commit 295b4eb

Please sign in to comment.