diff --git a/.envdev b/.envdev index e7fa7f4..b56f78d 100644 --- a/.envdev +++ b/.envdev @@ -17,4 +17,4 @@ DB_LOGGING= SECRET_ACCESS=access # Select the default number of articles returned by a GET request -QUERY_LIMIT=10 \ No newline at end of file +PAGE_SIZE=10 \ No newline at end of file diff --git a/README.md b/README.md index 0f3834b..fdbbb48 100644 --- a/README.md +++ b/README.md @@ -22,112 +22,248 @@ Content-Type: application/json # API +### `GET /news/:id?` + +Get either an array of articles (newest first), or a specified article if the optional "id" parameter is given. + +#### Response Body + +```jsonc +[{ + // [Number] index of the article + "index": index, + + // [String] author of the article + "author": author, + + // [String] raw body of the article + "body": body, + + // [Number] number of times this article has been edited + "edits": edits, + + // [String] body of the article rendered as HTML + "rendered": rendered, + + // [String] title of the article + "title": title, + + // [Date] time article was created + "createdAt": createdAt, + + // [Date] time article was updated + "updatedAt": updatedAt, +}] ``` -//NOTE: GET will return an empty array if a specific article can't be found -//NOTE: you can add a "limit" query parameter to change the default limit -GET /news?limit=10 +#### Available Query Parameters -### +- `fields` + - TYPE: `string` + A comma separated list of the field names you want returning, (index will always be returned) +- `page` + - TYPE: `number` + The current page you want returning +- `page_size` + - TYPE: `number` + The number of results to return. This superseeds the `PAGE_SIZE` environment variable for the query +> **NOTE** +> If a specific article is requested, then just that article is returned rather than an array -//DOCS: get latest news, up to a default limit, or specify the index "id" -GET /news/:id +### `GET /news/archive/:id?` +Get either an array of articles (oldest first), or a specified article if the optional "id" parameter is given. -### +#### Response Body +```jsonc +[{ + // [Number] index of the article + "index": index, -//DOCS: get the news starting from the beginning, up to a default limit, or specify the index "id" -GET /news/archive/:id + // [String] author of the article + "author": author, -//DOCS: result (if only a single article is specified, returns just that article rather than an array): -[ - { - "index": index, //absolute index of the result - "title": title, //title of the article - "author": author, //author of the aricle - "body": body, //body of the article - "rendered": rendered //body rendered as HTML - "edits": edits //number of times this article has been edited - "createdAt": createdAt //time created - "updatedAt": updatedAt //time updated - }, - ... -] + // [String] raw body of the article + "body": body, + // [Number] number of times this article has been edited + "edits": edits, -### + // [String] body of the article rendered as HTML + "rendered": rendered, + // [String] title of the article + "title": title, -//DOCS: get the latest metadata, up to a default limit, or specify the index "id" -GET /news/metadata/:id + // [Date] time article was created + "createdAt": createdAt, - -### - - -//DOCS: get the metadata starting from the beginning, up to a default limit, or specify the index "id" -GET /news/archive/metadata/:id - -//DOCS: result (if only a single article is specified, returns just that article rather than an array): -[ - { - "index": index, //absolute index of the result - "title": title, //title of the article - "author": author //author of the article - "edits": edits //number of times this article has been edited - "createdAt": createdAt //time created - "updatedAt": updatedAt //time updated - }, - ... -] - - -### - - -//DOCS: send a formatted JSON object, returns new index on success, or error on failure -POST /news -Authorization: Bearer XXX - -{ - "title": title //title of the article - "author": author //author of the article - "body": body //body of the article -} - -//DOCS: result (status 200 on success, otherwise an error status): -{ - "index": index //new index of the article -} - - -### - - -//DOCS: similar to `POST /news`, but allows overwriting an existing article -PATCH /news/:id -Authorization: Bearer XXX - -{ - "title": title //title of the article, optional - "author": author //author of the article, optional - "body": body //body of the article, optional -} - -//DOCS: result: status 200 on success, otherwise an error status - - -### - - -//DOCS: remove an article from the news feed -DELETE /news/:id -Authorization: Bearer XXX - -//DOCS: result: status 200 on success, otherwise an error status - - -### + // [Date] time article was updated + "updatedAt": updatedAt, +}] ``` + +#### Available Query Parameters + +- `fields` + - TYPE: `string` + A comma separated list of the field names you want returning, (index will always be returned) +- `page` + - TYPE: `number` + The current page you want returning +- `page_size` + - TYPE: `number` + The number of results to return. This superseeds the `PAGE_SIZE` environment variable for the query + +> **NOTE** +> If a specific article is requested, then just that article is returned rather than an array + +### `GET /news/metadata/:id?` + +Get either an array of metadata (newest first), or a specified article's metadata if the optional "id" parameter is given. + +#### Response Body + +```jsonc +[{ + // [Number] index of the article + "index": index, + + // [String] author of the article + "author": author, + + // [Number] number of times this article has been edited + "edits": edits, + + // [String] title of the article + "title": title, + + // [Date] time article was created + "createdAt": createdAt, + + // [Date] time article was updated + "updatedAt": updatedAt, +}] +``` + +#### Available Query Parameters + +- `fields` + - TYPE: `string` + A comma separated list of the field names you want returning, (index will always be returned) +- `page` + - TYPE: `number` + The current page you want returning +- `page_size` + - TYPE: `number` + The number of results to return. This superseeds the `PAGE_SIZE` environment variable for the query + +> **NOTE** +> If a specific article is requested, then just that article is returned rather than an array + +### `GET /news/archive/metadata/:id?` + +Get either an array of metadata (oldest first), or a specified article's metadata if the optional "id" parameter is given. + +#### Response Body + +```jsonc +[{ + // [Number] index of the article + "index": index, + + // [String] author of the article + "author": author, + + // [Number] number of times this article has been edited + "edits": edits, + + // [String] title of the article + "title": title, + + // [Date] time article was created + "createdAt": createdAt, + + // [Date] time article was updated + "updatedAt": updatedAt, +}] +``` + +#### Available Query Parameters + +- `fields` + - TYPE: `string` + A comma separated list of the field names you want returning, (index will always be returned) +- `page` + - TYPE: `number` + The current page you want returning +- `page_size` + - TYPE: `number` + The number of results to return. This superseeds the `PAGE_SIZE` environment variable for the query + +> **NOTE** +> If a specific article is requested, then just that article is returned rather than an array + +--- + +### `POST /news` + +> **IMPORTANT** +> Requires valid JWT Authorization header (Authorization: Bearer XXX) + +Create a new article resource, returns either the new article's index on success, or an error on failure. + +#### Request Body + +```jsonc +{ + // [String] OPTIONAL: title of the article + "title": title, + + // [String] OPTIONAL: author of the article + "author": author, + + // [String] OPTIONAL: body of the article + "body": body, +} +``` + +#### Response Body + +```jsonc +{ + // [Number]: new index of the article + "index": index, +} +``` + +### `PATCH /news/:id` + +> **IMPORTANT** +> Requires valid JWT Authorization header (Authorization: Bearer XXX) + +Update an existing article resource, returns either status code 200 on success, or an error status on failure. + +#### Request Body + +```jsonc +{ + // [String] OPTIONAL: title of the article + "title": title, + + // [String] OPTIONAL: author of the article + "author": author, + + // [String] OPTIONAL: body of the article + "body": body, +} +``` + +### `DELETE /news/:id` + +> **IMPORTANT** +> Requires valid JWT Authorization header (Authorization: Bearer XXX) + +Remove an existing article resource from the news feed, returns either status code 200 on success, or an error status on failure. \ No newline at end of file diff --git a/configure-script.js b/configure-script.js index f9ec71b..0cc51e0 100644 --- a/configure-script.js +++ b/configure-script.js @@ -66,7 +66,7 @@ services: - DB_USERNAME=${appDBUser} - DB_PASSWORD=${appDBPass} - DB_TIMEZONE=Australia/Sydney - - QUERY_LIMIT=10 + - PAGE_SIZE=10 - SECRET_ACCESS=${appSecretAccess} networks: - app-network @@ -85,9 +85,7 @@ services: - ./mysql:/var/lib/mysql - ./startup.sql:/docker-entrypoint-initdb.d/startup.sql:ro traefik_${appName}: - container_name: ${appName}_traefik image: "traefik:v2.4" - container_name: "traefik" command: - "--log.level=ERROR" - "--api.insecure=false" @@ -113,7 +111,7 @@ networks: const dockerfile = ` FROM node:18-bullseye-slim WORKDIR "/app" -COPY package*.json ./ +COPY package*.json /app RUN npm install --production COPY . /app EXPOSE ${appPort} diff --git a/server/news/index.js b/server/news/index.js index 2ceed03..35ee33e 100644 --- a/server/news/index.js +++ b/server/news/index.js @@ -12,14 +12,10 @@ const edit = require('./edit'); const remove = require('./remove'); //basic route management (all query possibilities) -router.get('/', cors(), query(false, false)); -router.get('/:id(\\d+)', cors(), query(false, false)); -router.get('/archive', cors(), query(true, false)); -router.get('/archive/:id(\\d+)', cors(), query(true, false)); -router.get('/metadata', cors(), query(false, true)); -router.get('/metadata/:id(\\d+)', cors(), query(false, true)); -router.get('/archive/metadata', cors(), query(true, true)); -router.get('/archive/metadata/:id(\\d+)', cors(), query(true, true)); +router.get('/:id(\\d+)?', cors(), query(false, false)); +router.get('/archive/:id(\\d+)?', cors(), query(true, false)); +router.get('/metadata/:id(\\d+)?', cors(), query(false, true)); +router.get('/archive/metadata/:id(\\d+)?', cors(), query(true, true)); //use middleware to authenticate the rest of the routes router.use(cors({ diff --git a/server/news/query.js b/server/news/query.js index fe7d1c3..3b0dcf0 100644 --- a/server/news/query.js +++ b/server/news/query.js @@ -1,18 +1,45 @@ -const { Op } = require('sequelize'); const { articles } = require('../database/models'); //the query function that can be reused const query = (ascending, metadataOnly) => async (req, res) => { + if (process.env.QUERY_LIMIT) { + process.env.PAGE_SIZE = process.env.QUERY_LIMIT; + console.warn('The use of QUERY_LIMIT is deprecated. Please use PAGE_SIZE instead.'); + } + + if (req.query.limit) { + req.query.page_size = req.query.limit; + console.warn('The use of the limit parameter is deprecated. Please use page_size instead.'); + } + + const PAGE_SIZE = parseInt(req.query.page_size) || parseInt(process.env.PAGE_SIZE) || 999; + const PAGE = parseInt(req.query.page) || 1; + const ARTICLE_ID = req.params.id ? parseInt(req.params.id) : undefined; + const FIELDS = req.query.fields ? req.query.fields.split(',') : undefined; + + const attributes = [ + 'index', + 'author', + 'createdAt', + 'edits', + 'title', + 'updatedAt', + ].concat(metadataOnly ? [] : [ + 'body', + 'rendered' + ]); + + //filter out attributes that aren't requested + const attributesToFetch = FIELDS ? attributes.filter((attr) => { + return FIELDS.includes(attr) || attr === 'index'; + }) : attributes; + //specific search (id is defined) - if (req.params.id && typeof(parseInt(req.params.id)) === 'number') { + if (typeof(ARTICLE_ID) === 'number' && !isNaN(ARTICLE_ID)) { const result = await articles.findOne({ - attributes: [ - 'index', 'title', 'author', 'edits', 'createdAt', 'updatedAt', ...(!metadataOnly ? ['body', 'rendered'] : []) - ], + attributes: attributesToFetch, where: { - index: { - [Op.eq]: ascending ? parseInt(req.params.id) : (await articles.max('index')) - parseInt(req.params.id) + 1 - } + index: ascending ? ARTICLE_ID : (await articles.max('index') - ARTICLE_ID) + 1, } }); @@ -23,16 +50,16 @@ const query = (ascending, metadataOnly) => async (req, res) => { //default search else { const result = await articles.findAndCountAll({ - attributes: [ - 'index', 'title', 'author', 'edits', 'createdAt', 'updatedAt', ...(!metadataOnly ? ['body', 'rendered'] : []) - ], + attributes: attributesToFetch, + limit: PAGE_SIZE, + offset: Math.max((PAGE - 1) * PAGE_SIZE, 0), order: [ ['index', ascending ? 'ASC' : 'DESC'] - ], - limit: parseInt(req.query.limit) || parseInt(process.env.QUERY_LIMIT) || 999 + ] }); - return res.status(200).json(result.rows || result); + //result is empty array if failed to find + return res.status(200).json(result.rows || result || []); } };