api copied to hudozka-api module based on express

This commit is contained in:
tmshv committed 2020-02-15 01:38:07 +03:00
1 parent 70007d3199
commit fc0724a900
21 files changed
+1093

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
{
"name": "hudozka-api",
"private": true,
"version": "1.0.0",
"description": "",
"main": "src/server.ts",
"scripts": {
"start": "ts-node src/server.ts",
"sitemap": "node scripts/sitemap.js",
"feed": "node scripts/feed.js"
},
"repository": {
"type": "git",
"url": "https://github.com/tmshv/hudozka"
},
"author": "Roman Timashev",
"license": "ISC",
"dependencies": {
"axios": "^0.19.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"feed": "^4.0.0",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"mongodb": "^3.5.3",
"sitemap": "^1.5.0"
},
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/koa": "^2.11.1",
"@types/koa-route": "^3.2.4",
"@types/mongodb": "^3.3.16",
"@types/node": "^13.7.1",
"ts-node": "^8.6.2",
"typescript": "^3.7.5"
}
}
@@ -0,0 +1,47 @@
import { Response, Request } from 'express'
import Album from '../core/Album'
export async function getItem(req: Request, res: Response) {
const id = req.params.slug
const album = await Album.findById(id)
if (album) {
res.json(album)
} else {
res.status(404)
res.json({
error: `Resource ${id} not found`
})
}
}
export async function getAll(req: Request, res: Response) {
const data = await Album.find({}, { sort: { date: -1 } })
if (data) {
res.json({
items: data,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
export async function getUrls (req: Request, res: Response) {
const data = await Album.find({})
if (data) {
const items = data.map(x => `/album/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
@@ -0,0 +1,55 @@
import Article from '../core/Article'
import { encodeArticle, findArticles } from '../server/articles'
import { Request, Response } from 'express'
const int = (value: string | string[]) => parseInt(Array.isArray(value)
? value[0]
: value
)
export async function getItem(req: Request, res: Response) {
const id = req.params.slug
const article = await Article.findById(id)
if (article) {
res.json(encodeArticle(article))
} else {
res.status(404)
res.json({
error: `Resource ${id} not found`
})
}
}
export async function getAll(req: Request, res: Response) {
const { query } = req
const page = int(query.page)
const pageSize = int(query.pageSize)
const data = await findArticles(page, pageSize)
if (data) {
res.json(data)
} else {
res.status(404)
res.json({
error: `Articles not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Article.find({})
if (data) {
const items = data.map(x => `/article/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
@@ -0,0 +1,51 @@
import { Request, Response } from 'express'
import Document from '../core/Document'
import { encodeFile } from '../api/files'
export async function getItem(req: Request, res: Response) {
const id = req.params.slug
const document = await Document.findById(id)
if (document) {
res.json(encodeFile(document))
} else {
res.status(404)
res.json({
error: `Resource ${id} not found`
})
}
}
export async function getAll(req: Request, res: Response) {
const documents = await Document.find({ hidden: false })
if (documents) {
const items = documents
.map(encodeFile)
.filter(x => x.imageUrl) // skip documents without preview image; needs to be refactored
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Document.find({})
if (data) {
const items = data.map(x => `/document/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
@@ -0,0 +1,17 @@
import Image from '../core/Image'
import { encodeImage } from '../api/image'
import { Request, Response } from 'express'
export async function getItem(req: Request, res: Response) {
const file = req.params.slug
const image = await Image.findByFile(file)
if (image) {
res.json(encodeImage(image))
} else {
res.status(404)
res.json({
error: `Resource ${file} not found`
})
}
}
@@ -0,0 +1,51 @@
import { Request, Response } from 'express'
import Page from '../core/Page'
import { getPathWithNoTrailingSlash } from '../lib/url'
import { encodePage } from '../api/page'
export async function getItem(req: Request, res: Response) {
const id = req.params.slug
const page = getPathWithNoTrailingSlash(id)
const resource = await Page.findByUrl(page)
if (resource) {
res.json(encodePage(resource))
} else {
res.status(404)
res.json({
error: `Resource ${page} not found`
})
}
}
export async function getAll(req: Request, res: Response) {
const items = await Page.find({})
if (items) {
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Page.find({})
if (data) {
const items = data.map(x => x.url)
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
@@ -0,0 +1,60 @@
import Teacher from '../core/Teacher'
import { encodePerson } from '../api/persons'
import { Request, Response } from 'express'
import { prioritySort } from '../lib/sort'
export async function getItem(req: Request, res: Response) {
const id = req.params.slug
const data = await Teacher.findById(id)
if (data) {
res.json(encodePerson(data))
} else {
res.status(404)
res.json({
error: `Resource ${id} not found`
})
}
}
export async function getAll(req: Request, res: Response) {
const order = [
'mg-timasheva',
'va-sarzhin',
'od-gogoleva',
'my-valkova',
'nv-andreeva',
'vv-voronova',
]
const teachersSorted = prioritySort.bind(null, [...order], t => t.id)
const teachers = await Teacher.find({ hidden: false })
const data = teachersSorted(teachers).map(encodePerson)
if (data) {
res.json({
items: data,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Teacher.find({})
if (data) {
const items = data.map(x => `/teacher/${x.id}`)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
@@ -0,0 +1,18 @@
import { findSchedule } from '../server/schedule'
import { Request, Response } from 'express'
import { collection } from '../db'
export async function getItem(req: Request, res: Response) {
const period = req.query.period
const semester = req.query.semester
const data = await findSchedule(collection, period, semester)
if (data) {
res.json(data)
} else {
res.status(404)
res.json({
error: `Schedule ${period}:${semester} not found`
})
}
}
+65
View File
@@ -0,0 +1,65 @@
import Data from './Data'
import Image from './Image'
import ImageArtifactType from './ImageArtifactType'
import { find, findOne } from '../lib/store'
const previewTypes = [
ImageArtifactType.SMALL,
ImageArtifactType.MEDIUM,
ImageArtifactType.BIG,
ImageArtifactType.ORIGIN,
]
const store = () => Data.getStore('Album')
export default class Album {
public id: string
static async find(query, options = {}): Promise<Album[]> {
const items = await find(store(), query, options as any)
return Promise.all(items.map(processAlbum))
}
static async findById(id) {
const item = await findOne(store(), { id })
return !item
? null
: processAlbum(item)
}
}
const previewFromImage = (imgs = []) => imgs.length
? imgs[0]
: null
const processAlbum = async album => {
let preview = {}
// {
// data: [
// {
// url: null,
// width: 0,
// height: 0,
// }
// ]
// }
const previewImageId = album.preview ? album.preview : previewFromImage(album.images)
if (previewImageId) {
const image = await Image.findById(previewImageId)
if (image) {
// const imageArtifacts = image.findArtifact(previewTypes)
// if (imageArtifacts) {
// preview = imageArtifacts
// }
preview = image.getPicture(ImageArtifactType.SMALL)
}
}
return {
...album,
preview,
url: `/album/${album.id}`,
}
}
+134
View File
@@ -0,0 +1,134 @@
import Data from './Data'
import Image from './Image'
import Config from './Config'
import { find, findOne, total } from '../lib/store'
const store = () => Data.getStore<Article>('Article')
function make(C) {
return x => new C(x)
}
export default class Article {
static async find(query, options = {}): Promise<Article[]> {
const items = await find(store(), query, options as any)
return Promise.all(items.map(processArticle))
}
static async findById(id) {
const item = await findOne(store(), { id })
return !item
? null
: processArticle(item)
}
static async total(query = {}) {
return total(store(), query)
}
static async findPinned() {
const now = new Date()
const query = { until: { $gte: now } }
return Article.find(query, {
sort: { date: -1 },
})
}
public id: any
public images: any
public post: any
public folder: any
public date: any
public title: any
public until: any
public tags: any
public content: any
public hash: any
public type: any
public origin: any
public file: any
public version: any
public preview: any
public url: any
constructor(data) {
this.id = data.id
this.images = data.images
this.post = data.post
this.folder = data.folder
this.date = data.date
this.title = data.title
this.until = data.until
this.tags = (data.tags || []).map(make(Tag))
this.content = data.content
this.hash = data.hash
this.type = data.type
this.origin = data.origin
this.file = data.file
this.version = data.version
this.preview = data.preview
this.url = `/article/${this.id}`
}
plain() {
return {
id: this.id,
images: this.images,
post: this.post,
folder: this.folder,
date: this.date,
title: this.title,
until: this.until,
tags: this.tags,
content: this.content,
hash: this.hash,
type: this.type,
origin: this.origin,
file: this.file,
version: this.version,
preview: this.preview,
url: this.url,
}
}
}
class Tag {
public name: string
constructor(tag) {
this.name = tag;
}
}
const previewFromImage = (imgs = []) => imgs.length
? imgs[0]
: null
async function resolveDefaultPreview() {
const config = await Config.findConfig()
if (!config) return null
return Image.findByFile(config.articleCardDefaultPreview)
}
async function processArticle(article) {
let preview = null
const previewId = article.preview
? article.preview
: previewFromImage(article.images)
if (previewId) {
preview = await Image.findById(previewId)
} else {
preview = await resolveDefaultPreview()
}
return new Article({
...article,
preview,
})
}
+42
View File
@@ -0,0 +1,42 @@
import Data from './Data'
import { findOne } from '../lib/store'
const store = () => Data.getStore('Settings')
export default class Config {
static async findConfig() {
const config = await Config.findById('settings')
return config || new Config({} as any)
}
static async findById(id: string) {
const data = await findOne(store(), { id })
if (!data) return null
return new Config(data)
}
public hash: any
public file: any
public server: any
public articlesPerPage: any
public collectiveOrder: any
public articleCardDefaultPreview: any
public collectiveImage: any
public redirect: any
constructor({ hash, file, ...data }) {
this.hash = hash
this.file = file
this.server = data.server
this.articlesPerPage = data.articlesPerPage || 5
this.collectiveOrder = data.collectiveOrder || []
this.articleCardDefaultPreview = data.articleCardDefaultPreview
this.collectiveImage = data.collectiveImage
const redirect = data.redirect || {}
this.redirect = new Map(Object
.entries(redirect)
)
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Collection } from 'mongodb'
import * as db from '../db'
const store = new Map<string, Collection>()
export default class Data {
static setStore(key: string, collection: Collection) {
store.set(key, collection)
}
static getStore<T>(key: string): Collection<T> {
return store.get(key)
}
static getCollection(name: string): Collection {
return db.collection(name)
}
}
+68
View File
@@ -0,0 +1,68 @@
import Data from './Data'
import Image from './Image'
import { find, findOne } from '../lib/store'
const store = () => Data.getStore('Document')
export default class Document {
static async find(query, options = {}): Promise<Document[]> {
const items = await find(store(), query, options as any)
return Promise.all(items.map(processDocument))
}
static async findById(id) {
const data = await findOne(store(), { id })
if (!data) return null
return processDocument(data)
}
public id: any
public hash: any
public file: any
public fileInfo: any
public type: any
public title: any
public category: any
public url: any
public preview: any
public viewUrl: any
constructor(data) {
this.id = data.id
this.hash = data.hash
this.file = data.file
// In the future: use fileInfo only
this.fileInfo = data.fileInfo
? new DocumentFile(data.fileInfo)
: new DocumentFile(data.file)
this.type = data.type
this.title = data.title
this.category = data.category
this.url = data.url
this.preview = data.preview
this.viewUrl = `/document/${this.id}`
}
}
class DocumentFile {
public name: any
public size: any
constructor(data) {
this.name = data.name
this.size = data.size
}
}
const processDocument = async data => {
const imageId = data.preview
const preview = await Image.findById(imageId)
return new Document({
...data,
preview,
})
}
+81
View File
@@ -0,0 +1,81 @@
import Data from './Data'
import ImageArtifactType from './ImageArtifactType'
import { findOne } from '../lib/store'
import { mapOf } from '../server/lib'
const store = () => Data.getStore('Image')
export default class Image {
static async findById(id) {
const data = await findOne(store(), { _id: id })
if (!data) return null
const artifacts = data.data
return new Image({ ...data, artifacts })
}
static async findByFile(file: string) {
const data = await findOne(store(), { file })
if (!data) return null
const artifacts = data.data
return new Image({ ...data, artifacts })
}
public hash: any
public file: any
public artifacts: any
constructor({ hash, file, artifacts }) {
this.hash = hash
this.file = file
this.artifacts = mapOf(ImageArtifact, artifacts)
}
getArtifact(size) {
return this.artifacts.get(size)
}
findArtifact(sizes) {
for (let size of sizes) {
const a = this.getArtifact(size)
if (a) return a
}
return null
}
getPicture(size) {
const retinaSize = ImageArtifactType.retina(size)
const a = this.getArtifact(size)
const a2 = this.getArtifact(retinaSize)
const srcset = !a2
? []
: [{
url: a2.url,
density: ImageArtifactType.RETINA_DENSITY,
}]
return {
src: a.url,
width: a.width,
height: a.height,
set: srcset,
}
}
}
class ImageArtifact {
public url: any
public size: any
public width: any
public height: any
constructor({ url, size, width, height }) {
this.url = url
this.size = size
this.width = width
this.height = height
}
}
@@ -0,0 +1,19 @@
class ImageArtifactType {
constructor() {
throw new Error('Cannot create instance of ImageArtifactType')
}
}
ImageArtifactType.SMALL = 'small'
ImageArtifactType.MEDIUM = 'medium'
ImageArtifactType.BIG = 'big'
ImageArtifactType.LARGE = 'large'
ImageArtifactType.ORIGIN = 'original'
ImageArtifactType.ORIGINAL = 'original'
ImageArtifactType.FACEBOOK = 'fb'
ImageArtifactType.RETINA_DENSITY = 2
ImageArtifactType.retina = type => `${type}@${ImageArtifactType.RETINA_DENSITY}`
module.exports = ImageArtifactType
+104
View File
@@ -0,0 +1,104 @@
import Data from './Data'
import { find as storeFind, findOne, total } from '../lib/store'
import Image from './Image'
import Config from './Config'
const store = () => Data.getStore('Page')
export default class Page {
static async find(query, options = {}): Promise<Page[]> {
const items = await storeFind(store(), query, options as any)
return Promise.all(items.map(processPage))
}
static async findById(id) {
const item = await findOne(store(), { id })
return !item
? null
: processPage(item)
}
static async findByUrl(url) {
const item = await findOne(store(), { url })
return !item
? null
: processPage(item)
}
static async total(query = {}) {
return total(store(), query)
}
public id: any
public hash: any
public file: any
public url: string
public content: any
public title: any
public data: any
public images: any
public documents: any
public preview: any
public tokens: any[]
constructor(data) {
this.id = data.id
this.hash = data.hash
this.file = data.file
this.url = data.url
this.content = data.content
this.title = data.title
this.data = data.data
this.images = data.images
this.documents = data.documents
this.preview = data.preview
this.tokens = data.tokens
}
plain() {
return {
id: this.id,
images: this.images,
documents: this.documents,
data: this.data,
title: this.title,
content: this.content,
hash: this.hash,
file: this.file,
preview: this.preview,
url: this.url,
}
}
}
const previewFromImage = (imgs = []) => imgs.length
? imgs[0]
: null
async function resolveDefaultPreview() {
const config = await Config.findConfig()
if (!config) return null
return Image.findByFile(config.articleCardDefaultPreview)
}
async function processPage(article) {
let preview = null
const previewId = article.preview
? article.preview
: previewFromImage(article.images)
if (previewId) {
preview = await Image.findById(previewId)
} else {
preview = await resolveDefaultPreview()
}
return new Page({
...article,
preview,
})
}
+87
View File
@@ -0,0 +1,87 @@
import Data from './Data'
import Image from './Image'
import { find, findOne } from '../lib/store'
const store = () => Data.getStore('Teacher')
export default class Teacher {
static async find(query, options = {}): Promise<Teacher[]> {
const items = await find(store(), query, options as any)
return Promise.all(items.map(processProfile))
}
static async findById(id) {
const data = await findOne(store(), { id })
if (!data) return null
return processProfile(data)
}
public id: any
public post: any
public file: any
public edu: any
public picture: any
public preview: any
public diploma: any
public position: any
public name: any
public status: any
public hash: any
public url: any
constructor(data) {
this.id = data.id
this.post = data.post
this.file = data.file
this.edu = data.edu
this.picture = data.picture
this.preview = data.preview
this.diploma = data.diploma
this.position = data.position
this.name = data.name
this.status = data.status
this.hash = data.hash
this.url = `/teacher/${this.id}`
}
/**
*
* "First Middle Last" -> "F. M. Last"
*
* @returns {string}
*/
shortName() {
const f = i => i.replace(/^(.).+/, '$1')
const [first, middle, last] = this.splitName()
return `${f(first)}. ${f(middle)}. ${last}`
}
/**
*
* "First Middle Last" -> ["First", "Middle", "Last"]
*
* @returns {Array|{index: number, input: string}}
*/
splitName() {
const name = this.name
const m = /(.+)\s(.+)\s(.+)/.exec(name)
return m.slice(1)
}
}
const processProfile = async profile => {
const imageId = profile.picture
const previewId = profile.preview
const picture = await Image.findById(imageId)
const preview = await Image.findById(previewId)
return new Teacher({
...profile,
picture,
preview,
})
}
+19
View File
@@ -0,0 +1,19 @@
import {c} from '../core/db';
const PAGES = 'pages';
export async function getPageUrls() {
return await c(PAGES)
.find({}, {url: 1})
.toArray();
}
/**
* Find a person by it's id
*
* @param {string} url
* @returns {Promise|*}
*/
export async function getPage(url) {
return await c(PAGES).findOne({url: url});
}
+38
View File
@@ -0,0 +1,38 @@
import Data from './core/Data'
import { MongoClient, ObjectId, Db } from 'mongodb'
var db: Db
export async function connect(dbName: string, mongoUri: string) {
if (db) {
return db
}
const connection = await MongoClient.connect(mongoUri, { useUnifiedTopology: true })
db = init(connection.db(dbName))
return db
}
export function collection(name) {
return db.collection(name)
}
export function id(i) {
if (typeof i === 'string') return new ObjectId(i)
else if (i instanceof ObjectId) return i
else if (i instanceof Object) return '_id' in i ? new ObjectId(i._id) : null
return null
}
function init(db: Db) {
console.log('INIT DB')
Data.setStore('Page', db.collection('pages'))
Data.setStore('Article', db.collection('articles'))
Data.setStore('Album', db.collection('albums'))
Data.setStore('Image', db.collection('images'))
Data.setStore('Teacher', db.collection('collective'))
Data.setStore('Document', db.collection('documents'))
Data.setStore('Settings', db.collection('settings'))
return db
}
+58
View File
@@ -0,0 +1,58 @@
import dotenv from 'dotenv'
import express from 'express'
import { connect } from './db'
import * as person from './controllers/person'
import * as article from './controllers/article'
import * as album from './controllers/album'
import * as schedule from './controllers/schedule'
import * as file from './controllers/file'
import * as page from './controllers/page'
import * as image from './controllers/image'
dotenv.config()
async function main() {
const port = parseInt(process.env.PORT, 10) || 3000
const mongoUri = process.env.MONGO_URI
try {
await connect('hudozka', mongoUri)
console.log('connected')
} catch (e) {
console.error(e.message)
process.exit(1)
}
const app = express()
app.get('/persons', person.getAll)
app.get('/persons/urls', person.getUrls)
app.get('/persons/:slug', person.getItem)
app.get('/articles', article.getAll)
app.get('/articles/urls', article.getUrls)
app.get('/articles/:slug', article.getItem)
app.get('/albums', album.getAll)
app.get('/albums/urls', album.getUrls)
app.get('/albums/:slug', album.getItem)
app.get('/files', file.getAll)
app.get('/files/urls', file.getUrls)
app.get('/files/:slug', file.getItem)
app.get('/pages', page.getAll)
app.get('/pages/urls', page.getUrls)
app.get('/pages/:slug', page.getItem)
app.get('/images/:slug', image.getItem)
app.get('/schedule', schedule.getItem)
app.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`)
})
}
main()
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true
},
"exclude": [
"node_modules"
],
"include": [
"next-env.d.ts",
"**/*.ts"
]
}