-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into weston/contactRequest
- Loading branch information
Showing
8 changed files
with
344 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,27 @@ | ||
import { json } from "body-parser"; | ||
import express from "express"; | ||
import mongoose from "mongoose"; | ||
|
||
import { port } from "./config"; | ||
import { errorHandler } from "./errors/handler"; | ||
import contactRoute from "./routes/contactRequest"; | ||
import { mongoUri, port } from "./config"; | ||
import articleRoutes from "./routes/article"; | ||
|
||
// Initialize Express App | ||
const app = express(); | ||
|
||
app.use(json()); | ||
app.use("/api", contactRoute); | ||
app.use("/api/articles", articleRoutes); | ||
app.use(errorHandler); | ||
app.listen(port, () => { | ||
console.log(`> Listening on port ${port}`); | ||
}); | ||
mongoose | ||
.connect(mongoUri) | ||
.then(() => { | ||
console.log("Mongoose connected!"); | ||
// Tell app to listen on our port environment variable | ||
app.listen(port, () => { | ||
console.log(`> Listening on port ${port}`); | ||
}); | ||
}) | ||
.catch(console.error); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { RequestHandler } from "express"; | ||
import { validationResult } from "express-validator"; | ||
import createHttpError from "http-errors"; | ||
|
||
import ArticleModel from "../models/article"; | ||
import validationErrorParser from "../util/validationErrorParser"; | ||
|
||
type ArticleUpdate = { | ||
header: string; | ||
dateCreated: Date; | ||
author: string; | ||
body?: string | null; | ||
thumbnail?: string | null; | ||
}; | ||
|
||
export const createArticle: RequestHandler = async (req, res, next) => { | ||
const errors = validationResult(req); | ||
try { | ||
validationErrorParser(errors); | ||
const article = await ArticleModel.create(req.body); | ||
res.status(201).json(article); | ||
} catch (error) { | ||
next(error); | ||
} | ||
}; | ||
|
||
export const getAllArticles: RequestHandler = async (req, res, next) => { | ||
try { | ||
const articles = await ArticleModel.find(); | ||
res.status(200).json(articles); | ||
} catch (error) { | ||
next(error); | ||
} | ||
}; | ||
|
||
export const updateArticle: RequestHandler = async (req, res, next) => { | ||
const errors = validationResult(req); | ||
try { | ||
const { id } = req.params; | ||
validationErrorParser(errors); | ||
|
||
// Returns the updated article | ||
const reqBody: ArticleUpdate = req.body as ArticleUpdate; | ||
const article = await ArticleModel.findByIdAndUpdate(id, reqBody, { | ||
new: true, | ||
runValidators: true, | ||
}); | ||
|
||
// Check if article of id exists | ||
if (article === null) { | ||
throw createHttpError(404, "Article not found at id " + id); | ||
} | ||
|
||
res.status(200).json(article); | ||
} catch (error) { | ||
next(error); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { InferSchemaType, Schema, model } from "mongoose"; | ||
|
||
const articleSchema = new Schema({ | ||
// Header/title of article | ||
header: { type: String, required: true }, | ||
|
||
// Creation date of article | ||
dateCreated: { type: Date, required: true }, | ||
|
||
// Author of article | ||
author: { type: String, required: true }, | ||
|
||
// Text content of article | ||
body: { type: String }, | ||
|
||
// File URL to article image | ||
thumbnail: { type: String }, | ||
}); | ||
|
||
type ArticleItem = InferSchemaType<typeof articleSchema>; | ||
|
||
export default model<ArticleItem>("ArticleItem", articleSchema); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import express from "express"; | ||
|
||
import * as ArticleController from "../controllers/article"; | ||
import * as ArticleValidator from "../validators/article"; | ||
|
||
const router = express.Router(); | ||
|
||
router.get("/all", ArticleController.getAllArticles); | ||
router.post("/create", ArticleValidator.createArticle, ArticleController.createArticle); | ||
router.put("/:id", ArticleValidator.updateArticle, ArticleController.updateArticle); | ||
|
||
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { body } from "express-validator"; | ||
|
||
/** | ||
* Validators for creating and updating articles | ||
*/ | ||
const makeHeaderValidator = () => | ||
body("header") | ||
.exists() | ||
.withMessage("header is required") | ||
.bail() | ||
.isString() | ||
.withMessage("header must be a string") | ||
.bail() | ||
.notEmpty() | ||
.withMessage("header cannot be empty"); | ||
|
||
const makeDateCreatedValidator = () => | ||
body("dateCreated") | ||
.exists() | ||
.withMessage("dateCreated is required") | ||
.bail() | ||
.isISO8601() | ||
.withMessage("dateCreated must be a valid date-time string"); | ||
|
||
const makeAuthorValidator = () => | ||
body("author") | ||
.exists() | ||
.withMessage("author is required") | ||
.bail() | ||
.isString() | ||
.withMessage("author must be a string") | ||
.bail() | ||
.notEmpty() | ||
.withMessage("author cannot be empty"); | ||
|
||
const makeBodyValidator = () => | ||
body("body").optional().isString().withMessage("body must be a string"); | ||
|
||
const makeThumbnailValidator = () => | ||
body("thumbnail").optional().isURL().withMessage("thumbnail must be a url"); | ||
|
||
export const createArticle = [ | ||
makeHeaderValidator(), | ||
makeDateCreatedValidator(), | ||
makeAuthorValidator(), | ||
makeBodyValidator(), | ||
makeThumbnailValidator(), | ||
]; | ||
|
||
export const updateArticle = [ | ||
makeHeaderValidator(), | ||
makeDateCreatedValidator(), | ||
makeAuthorValidator(), | ||
makeBodyValidator(), | ||
makeThumbnailValidator(), | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
/** | ||
* Based on the TSE Onboarding API client implementation by benjaminJohnson2204: | ||
* https://github.com/TritonSE/onboarding/blob/main/frontend/src/api/requests.ts | ||
*/ | ||
|
||
/** | ||
* A custom type defining which HTTP methods we will handle in this file | ||
*/ | ||
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; | ||
|
||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? ""; | ||
|
||
/** | ||
* A wrapper around the built-in `fetch()` function that abstracts away some of | ||
* the low-level details so we can focus on the important parts of each request. | ||
* | ||
* @param method The HTTP method to use | ||
* @param url The URL to request | ||
* @param body The body of the request, or undefined if there is none | ||
* @param headers The headers of the request | ||
* @returns The Response object returned by `fetch() | ||
*/ | ||
async function fetchRequest( | ||
method: Method, | ||
url: string, | ||
body: unknown, | ||
headers: Record<string, string>, | ||
): Promise<Response> { | ||
const hasBody = body !== undefined; | ||
|
||
const newHeaders = { ...headers }; | ||
if (hasBody) { | ||
newHeaders["Content-Type"] = "application/json"; | ||
} | ||
|
||
const response = await fetch(url, { | ||
method, | ||
headers: newHeaders, | ||
body: hasBody ? JSON.stringify(body) : undefined, | ||
}); | ||
|
||
return response; | ||
} | ||
|
||
/** | ||
* Throws an error if the given response's status code indicates an error | ||
* occurred, else does nothing. | ||
* | ||
* @param response A response returned by `fetch()` or `fetchRequest()` | ||
* @throws An error if the response was not successful (200-299) or a redirect | ||
* (300-399) | ||
*/ | ||
async function assertOk(response: Response): Promise<void> { | ||
if (response.ok) { | ||
return; | ||
} | ||
|
||
let message = `${response.status.toString()} ${response.statusText}`; | ||
|
||
try { | ||
const text = await response.text(); | ||
if (text) { | ||
message += ": " + text; | ||
} | ||
} catch { | ||
// skip errors | ||
} | ||
|
||
throw new Error(message); | ||
} | ||
|
||
/** | ||
* Sends a GET request to the provided API URL. | ||
* | ||
* @param url The URL to request | ||
* @param headers The headers of the request (optional) | ||
* @returns The Response object returned by `fetch()` | ||
*/ | ||
export async function get(url: string, headers: Record<string, string> = {}): Promise<Response> { | ||
// GET requests do not have a body | ||
const response = await fetchRequest("GET", `${API_BASE_URL}${url}`, undefined, headers); | ||
await assertOk(response); | ||
return response; | ||
} | ||
|
||
/** | ||
* Sends a POST request to the provided API URL. | ||
* | ||
* @param url The URL to request | ||
* @param body The body of the request, or undefined if there is none | ||
* @param headers The headers of the request (optional) | ||
* @returns The Response object returned by `fetch()` | ||
*/ | ||
export async function post( | ||
url: string, | ||
body: unknown, | ||
headers: Record<string, string> = {}, | ||
): Promise<Response> { | ||
const response = await fetchRequest("POST", `${API_BASE_URL}${url}`, body, headers); | ||
await assertOk(response); | ||
return response; | ||
} | ||
|
||
/** | ||
* Sends a PUT request to the provided API URL. | ||
* | ||
* @param url The URL to request | ||
* @param body The body of the request, or undefined if there is none | ||
* @param headers The headers of the request (optional) | ||
* @returns The Response object returned by `fetch()` | ||
*/ | ||
export async function put( | ||
url: string, | ||
body: unknown, | ||
headers: Record<string, string> = {}, | ||
): Promise<Response> { | ||
const response = await fetchRequest("PUT", `${API_BASE_URL}${url}`, body, headers); | ||
await assertOk(response); | ||
return response; | ||
} | ||
|
||
/** | ||
* Sends a PATCH request to the provided API URL. | ||
* | ||
* @param url The URL to request | ||
* @param body The body of the request, or undefined if there is none | ||
* @param headers The headers of the request (optional) | ||
* @returns The Response object returned by `fetch()` | ||
*/ | ||
export async function patch( | ||
url: string, | ||
body: unknown, | ||
headers: Record<string, string> = {}, | ||
): Promise<Response> { | ||
const response = await fetchRequest("PATCH", `${API_BASE_URL}${url}`, body, headers); | ||
await assertOk(response); | ||
return response; | ||
} | ||
|
||
/** | ||
* Sends a DELETE request to the provided API URL. | ||
* | ||
* @param url The URL to request | ||
* @param headers The headers of the request (optional) | ||
* @returns The Response object returned by `fetch()` | ||
*/ | ||
export async function del(url: string, headers: Record<string, string> = {}): Promise<Response> { | ||
const response = await fetchRequest("DELETE", `${API_BASE_URL}${url}`, undefined, headers); | ||
await assertOk(response); | ||
return response; | ||
} | ||
|
||
export type APIData<T> = { success: true; data: T }; | ||
export type APIError = { success: false; error: string }; | ||
export type APIResult<T> = APIData<T> | APIError; | ||
|
||
/** | ||
* Helper function for API client functions to handle errors consistently. | ||
* Recommended usage is in a `catch` block--see `createTask` in `src/api/tasks` | ||
* for an example. | ||
* | ||
* @param error An error thrown by a lower-level API function | ||
* @returns An `APIError` object with a message from the given error | ||
*/ | ||
export function handleAPIError(error: unknown): APIError { | ||
if (error instanceof Error) { | ||
return { success: false, error: error.message }; | ||
} else if (typeof error === "string") { | ||
return { success: false, error }; | ||
} | ||
return { success: false, error: `Unknown error: ${String(error)}` }; | ||
} |