From 0ab9521e7a08a2903a53e7a3eefa5c1d6e509f2a Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Tue, 19 Apr 2022 17:42:07 +0000 Subject: [PATCH] Some development, first databse connection established --- api/config.mjs | 7 ++ api/page/model.mjs | 26 ++++++ api/page/routes.mjs | 16 ++++ api/page/security.mjs | 0 api/pagination/helpers.mjs | 124 ++++++++++++++++++++++++++ api/pagination/parser.mjs | 33 +++++++ api/serve.mjs | 38 ++++++++ api/{server_flaska.mjs => server.mjs} | 54 ++++++----- index.mjs | 2 +- package.json | 1 + 10 files changed, 272 insertions(+), 29 deletions(-) create mode 100644 api/page/model.mjs create mode 100644 api/page/routes.mjs create mode 100644 api/page/security.mjs create mode 100644 api/pagination/helpers.mjs create mode 100644 api/pagination/parser.mjs create mode 100644 api/serve.mjs rename api/{server_flaska.mjs => server.mjs} (68%) diff --git a/api/config.mjs b/api/config.mjs index 437cdaf..ea732e7 100644 --- a/api/config.mjs +++ b/api/config.mjs @@ -54,6 +54,13 @@ nconf.defaults({ "frontend": { "url": "http://beta01.nfp.moe" }, + "mssql": { + "floor": 1, + "ceiling": 2, + "heartbeatSecs": 20, + "inactivityTimeoutSecs": 60, + "connectionString": "Driver={ODBC Driver 17 for SQL Server}; Server=localhost;UID=dev; PWD=dev; Database=nfp_moe", + }, "fileSize": 524288000, "upload": { "baseurl": "https://cdn.nfp.is", diff --git a/api/page/model.mjs b/api/page/model.mjs new file mode 100644 index 0000000..c437b95 --- /dev/null +++ b/api/page/model.mjs @@ -0,0 +1,26 @@ +/* + +Page model: +{ + filename, + filetype, + small_image, + medium_image, + large_image, + *small_url, + *medium_url, + *large_url, + size, + staff_id, + is_deleted, + created_at, + updated_at, +} + +*/ + +export async function getTree(ctx) { + let res = await ctx.db.promises.callProc('pages_gettree', []) + console.log(res) + return {} +} diff --git a/api/page/routes.mjs b/api/page/routes.mjs new file mode 100644 index 0000000..503d427 --- /dev/null +++ b/api/page/routes.mjs @@ -0,0 +1,16 @@ +import * as Page from './model.mjs' +import * as security from './security.mjs' + +export default class PageRoutes { + constructor(opts = {}) { + Object.assign(this, { + Page: opts.Page || Page, + security: opts.security || security, + }) + } + + /** GET: /api/pagetree */ + async getPageTree(ctx) { + ctx.body = await this.Page.getTree(ctx) + } +} diff --git a/api/page/security.mjs b/api/page/security.mjs new file mode 100644 index 0000000..e69de29 diff --git a/api/pagination/helpers.mjs b/api/pagination/helpers.mjs new file mode 100644 index 0000000..0764b83 --- /dev/null +++ b/api/pagination/helpers.mjs @@ -0,0 +1,124 @@ +import _ from 'lodash' +import config from '../config.mjs' + +function limit(value, min, max, fallback) { + let out = parseInt(value, 10) + + if (!out) { + out = fallback + } + + if (out < min) { + out = min + } + + if (out > max) { + out = max + } + + return out +} + +export function parsePagination(ctx) { + let out = { + perPage: limit(ctx.query.perPage, 1, 1500, 1250), + page: limit(ctx.query.page, 1, Number.MAX_SAFE_INTEGER, 1), + } + + Object.keys(ctx.query).forEach(item => { + if (item.startsWith('perPage.')) { + let name = item.substring(8) + out[name] = { + perPage: limit(ctx.query[`perPage.${name}`], 1, 1500, 1250), + page: limit(ctx.query[`page.${name}`], 1, Number.MAX_SAFE_INTEGER, 1), + } + } + }) + + return out +} + +export function parseFilter(ctx) { + let where + let whereNot + + where = _.omitBy(ctx.query, test => test[0] === '!') + + whereNot = _.pickBy(ctx.query, test => test[0] === '!') + whereNot = _.transform( + whereNot, + (result, value, key) => (result[key] = value.slice(1)) + ) + + return { + where: pick => _.pick(where, pick), + whereNot: pick => _.pick(whereNot, pick), + includes: (ctx.query.includes && ctx.query.includes.split(',')) || [], + } +} + +export function generateLinks(ctx, total) { + let out = [] + + let base = _(ctx.query) + .omit(['page']) + .transform((res, val, key) => res.push(`${key}=${val}`), []) + .value() + + if (!ctx.query.perPage) { + base.push(`perPage=${ctx.state.pagination.perPage}`) + } + + // let protocol = ctx.protocol + + // if (config.get('frontend:url').startsWith('https')) { + // protocol = 'https' + // } + + let proto = 'http' + + if (config.get('frontend:url').startsWith('https')) { + proto = 'https' + } + + let first = new URL(ctx.path, proto + '://' + ctx.host).toString() + + first += `?${base.join('&')}` + + // Add the current page first + out.push({ + rel: 'current', + title: `Page ${ctx.query.page || 1}`, + url: `${first}`, + }) + + // Then add any previous pages if we can + if (ctx.state.pagination.page > 1) { + out.push({ + rel: 'previous', + title: 'Previous', + url: `${first}&page=${ctx.state.pagination.page - 1}`, + }) + out.push({ + rel: 'first', + title: 'First', + url: `${first}&page=1`, + }) + } + + // Then add any next pages if we can + if ((ctx.state.pagination.perPage * (ctx.state.pagination.page - 1)) + ctx.state.pagination.perPage < total) { + out.push({ + rel: 'next', + title: 'Next', + url: `${first}&page=${ctx.state.pagination.page + 1}`, + }) + out.push({ + rel: 'last', + title: 'Last', + url: `${first}&page=${Math.ceil(total / ctx.state.pagination.perPage)}`, + }) + } + + return out +} diff --git a/api/pagination/parser.mjs b/api/pagination/parser.mjs new file mode 100644 index 0000000..cdb6603 --- /dev/null +++ b/api/pagination/parser.mjs @@ -0,0 +1,33 @@ +import format from 'format-link-header' + +import * as pagination from './helpers.mjs' + +export default class ParserMiddleware { + constructor(opts = {}) { + Object.assign(this, { + pagination: opts.pagination || pagination, + format: opts.format || format, + }) + } + + contextParser() { + return (ctx) => { + ctx.state.pagination = this.pagination.parsePagination(ctx) + ctx.state.filter = this.pagination.parseFilter(ctx) + } + } + + generateLinks() { + return async (ctx, next) => { + await next() + + if (ctx.state.pagination.total > 0) { + ctx.set('Link', this.format(this.pagination.generateLinks(ctx, ctx.state.pagination.total))) + } + + if (ctx.state.pagination.total != null) { + ctx.set('pagination_total', ctx.state.pagination.total) + } + } + } +} diff --git a/api/serve.mjs b/api/serve.mjs new file mode 100644 index 0000000..4ab7123 --- /dev/null +++ b/api/serve.mjs @@ -0,0 +1,38 @@ +import path from 'path' +import { FileResponse, HttpError } from 'flaska' +import fs from 'fs/promises' + +export default class ServeHandler { + constructor(opts = {}) { + Object.assign(this, { + fs: opts.fs || fs, + root: opts.root, + }) + } + + /** GET: /::file */ + serve(ctx) { + if (ctx.params.file.startsWith('api/')) { + throw new HttpError(404, 'Not Found: ' + ctx.params.file, { status: 404, message: 'Not Found: ' + ctx.params.file }) + } + + let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html')) + + if (!file.startsWith(this.root)) { + ctx.status = 404 + ctx.body = 'HTTP 404 Error' + return + } + + return this.fs.stat(file).catch((err) => { + if (err.code === 'ENOENT') { + file = path.resolve(path.join(this.root, 'index.html')) + return this.fs.stat(file) + } + return Promise.reject(err) + }) + .then(function(stat) { + ctx.body = new FileResponse(file, stat) + }) + } +} \ No newline at end of file diff --git a/api/server_flaska.mjs b/api/server.mjs similarity index 68% rename from api/server_flaska.mjs rename to api/server.mjs index 0ca5fad..bbabc4e 100644 --- a/api/server_flaska.mjs +++ b/api/server.mjs @@ -1,16 +1,16 @@ -import path from 'path' -import fs from 'fs/promises' -import { Flaska, FileResponse, HttpError, QueryHandler } from 'flaska' +import { Flaska, QueryHandler } from 'flaska' +import MSSQL from 'msnodesqlv8' import config from './config.mjs' import PageRoutes from './page/routes.mjs' +import ServeHandler from './serve.mjs' // import ArticleRoutes from './article/routes.mjs' -import ParserMiddleware from './parser/middleware.mjs' +import ParserMiddleware from './pagination/parser.mjs' export function run(http, port, core) { let localUtil = new core.sc.Util(import.meta.url) - const staticRoot = localUtil.getPathFromRoot('../public') + // Create our server const flaska = new Flaska({ log: core.log, nonce: ['script-src'], @@ -24,7 +24,23 @@ export function run(http, port, core) { 'Cross-Origin-Embedder-Policy': 'require-corp', }, }, http) + + // Create our database pool + const pool = new MSSQL.Pool(config.get('mssql')) + core.log.info(config.get('mssql'), 'MSSQL database setttings') + + pool.on('open', function() { + core.log.info('MSSQL connection open') + }) + + pool.on('error', function(error) { + core.log.error(error, 'Error in MSSQL pool') + }) + + pool.open() + + // configure our server if (config.get('NODE_ENV') === 'development') { flaska.devMode() } @@ -33,10 +49,12 @@ export function run(http, port, core) { flaska.before(function(ctx) { ctx.state.started = new Date().getTime() + ctx.db = pool }) flaska.before(QueryHandler()) flaska.before(parser.contextParser()) + // flaska.after(function(ctx) { let ended = new Date().getTime() var requestTime = ended - ctx.state.started @@ -67,30 +85,10 @@ export function run(http, port, core) { // flaska.get('/api/articles/public/:id', article.getPublicSingleArticle.bind(article)) // flaska.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article)) - flaska.get('/::file', function(ctx) { - if (ctx.params.file.startsWith('api/')) { - throw new HttpError(404, 'Not Found: ' + ctx.params.file, { status: 404, message: 'Not Found: ' + ctx.params.file }) - } - - let file = path.resolve(path.join(staticRoot, ctx.params.file ? ctx.params.file : 'index.html')) - - if (!file.startsWith(staticRoot)) { - ctx.status = 404 - ctx.body = 'HTTP 404 Error' - return - } - - return fs.stat(file).catch(function(err) { - if (err.code === 'ENOENT') { - file = path.resolve(path.join(staticRoot, 'index.html')) - return fs.stat(file) - } - return Promise.reject(err) - }) - .then(function(stat) { - ctx.body = new FileResponse(file, stat) - }) + const serve = new ServeHandler({ + root: localUtil.getPathFromRoot('../public'), }) + flaska.get('/::file', serve.serve.bind(serve)) return flaska.listenAsync(port).then(function() { core.log.info('Server is listening on port ' + port) diff --git a/index.mjs b/index.mjs index 5c4bc37..1383212 100644 --- a/index.mjs +++ b/index.mjs @@ -3,7 +3,7 @@ import config from './api/config.mjs' export function start(http, port, ctx) { config.stores.overrides.store = ctx.config - return import('./api/server_flaska.mjs') + return import('./api/server.mjs') .then(function(server) { return server.run(http, port, ctx) }) diff --git a/package.json b/package.json index 099593c..c59d971 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "http-errors": "^1.7.2", "json-mask": "^0.3.8", "knex-core": "^0.19.5", + "msnodesqlv8": "^2.4.7", "nconf-lite": "^1.0.1", "parse-torrent": "^7.0.1", "pg": "^8.7.3",