rm hudozka-api

This commit is contained in:
tmshv committed 2026-04-24 20:26:30 +03:00
1 parent bc4e11d538
commit 1348eab71f
26 files changed
-2197

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
.env
*.tar
node_modules
-3
View File
@@ -1,3 +0,0 @@
node_modules
.env
*.tar
-27
View File
@@ -1,27 +0,0 @@
# BUILD
FROM node:12-alpine as build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# Building app
RUN npm run build
# RUNTIME
FROM node:12-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY --from=build /app/build ./build
EXPOSE 80
ENV PORT 80
ENV NODE_ENV production
# Running the app
CMD [ "npm", "start" ]
File diff suppressed because it is too large. Load diff
-36
View File
@@ -1,36 +0,0 @@
{
"name": "hudozka-api",
"private": true,
"version": "1.0.0",
"description": "",
"main": "src/server.ts",
"scripts": {
"start": "node build/server.js",
"dev": "ts-node src/server.ts",
"build": "tsc",
"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": {
"dotenv": "^8.2.0",
"express": "^4.22.1",
"feed": "^4.0.0",
"lodash": "^4.18.1",
"mongodb": "^3.7.4",
"sitemap": "^1.5.0"
},
"devDependencies": {
"@types/classnames": "^2.2.9",
"@types/express": "^4.17.2",
"@types/mongodb": "^3.3.16",
"@types/node": "^13.7.1",
"ts-node": "^8.6.2",
"typescript": "^3.7.5"
}
}
@@ -1,23 +0,0 @@
import { getPathWaterfall } from '../lib/url'
import { Breadcrumb } from '../types'
import Page from '../core/Page'
export async function getItems(path: string): Promise<Breadcrumb> {
const bc = getPathWaterfall(path)
const parts = await Promise.all(bc.map(async href => {
const resource = await Page.findByUrl(href)
if (!resource) {
return null
}
const name = resource.title
return {
name,
href,
}
}))
return parts.filter(Boolean)
}
@@ -1,32 +0,0 @@
import { Request, Response } from 'express'
import Document from '../core/Document'
import { encodeFile, encodeFileUrl } from '../factory/file'
export async function getItem(req: Request, res: Response) {
const slug = req.params.slug
const item = await Document.findBySlug(slug)
if (item) {
res.json(encodeFile(item))
} else {
res.status(404)
res.json({
error: `Resource ${slug} not found`
})
}
}
export async function getUrls(req: Request, res: Response) {
const data = await Document.find({})
if (data) {
const items = data.map(encodeFileUrl)
res.json({
items
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
@@ -1,39 +0,0 @@
import Image from '../core/Image'
import { encodeImage } from '../factory/image'
import { Request, Response } from 'express'
import { oid } from '../db'
export async function getItem(req: Request, res: Response) {
const slug = req.params.slug
const id = oid(slug)
if (!id) {
res.status(400)
return res.json({
error: `Failed to parse ${slug} as ObjectId`
})
}
const image = await Image.findById(id)
if (image) {
res.json(encodeImage(image))
} else {
res.status(404)
res.json({
error: `Resource ${slug} not found`
})
}
}
export async function getItemByFile(req: Request, res: Response) {
const file = req.query.file
const image = await Image.findByFile(file)
if (image) {
res.json(encodeImage(image))
} else {
res.status(404)
res.json({
error: `Resource ${file} not found`
})
}
}
-117
View File
@@ -1,117 +0,0 @@
import { pick } from 'lodash'
import { Request, Response } from 'express'
import Page from '../core/Page'
import { dropTrailingSlash, readQueryArray, readQueryInt, readQueryString } from '../lib/url'
import { encodePage } from '../factory/page'
import * as breadcrumb from './breadcrumb'
import { isArrayOk } from '../lib/array'
function getSort(sortBy: string) {
if (!sortBy) {
return null
}
if (sortBy.charAt(0) === '-') {
const key = sortBy.substr(1, sortBy.length)
return {
[key]: -1,
}
}
return {
[sortBy]: 1,
}
}
async function resolveItem(page: Page) {
const breadcrumbs = await breadcrumb.getItems(page.getUrl())
page.setBreadcrumb(breadcrumbs)
return page
}
export async function getItem(req: Request, res: Response) {
const pageUrl = dropTrailingSlash(req.query.page)
const resource = await Page.findByUrl(pageUrl)
if (resource) {
const page = await resolveItem(resource)
res.json(encodePage(page))
} else {
res.status(404)
res.json({
error: `Resource ${pageUrl} 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 getByTags(req: Request, res: Response) {
const skip = readQueryInt(req, 'skip')
const limit = readQueryInt(req, 'limit')
const sortBy = readQueryString(req, 'sortBy')
const sort = getSort(sortBy)
const fields = readQueryArray(req, 'field')
const tags = readQueryArray(req, 'tag')
if (tags.length === 0) {
res.status(400)
return res.json({
error: `No tags in request`
})
}
const options = { skip, limit, sort }
const models = await Page.find({
tags: { $in: tags },
}, options)
if (models) {
const pages = await Promise.all(models.map(resolveItem))
const encodedItems = pages.map(encodePage)
const items = isArrayOk(fields)
? encodedItems.map(x => pick(x, fields))
: encodedItems
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`
})
}
}
@@ -1,22 +0,0 @@
import Page from '../core/Page'
import { Request, Response } from 'express'
import { unique } from '../lib/array'
export async function getAll(req: Request, res: Response) {
const data = await Page.find({})
const tags = data
.flatMap(x => x.tags)
.filter(x => x.isValid())
const items = unique(tags, t => t.slug)
if (data) {
res.json({
items,
})
} else {
res.status(404)
res.json({
error: `Not found`
})
}
}
-18
View File
@@ -1,18 +0,0 @@
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
@@ -1,68 +0,0 @@
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 findBySlug(slug: string) {
const data = await findOne(store(), { id: slug })
if (!data) return null
return processDocument(data)
}
public id: any
public hash: any
public file: any
public fileInfo: FileInfo
public type: any
public title: any
public category: any
public url: any
public preview: Image
public viewUrl: any
constructor(data: { preview: Image, [name: string]: any }) {
this.id = data.id
this.hash = data.hash
this.file = data.file
// In the future: use fileInfo only
this.fileInfo = data.fileInfo
? new FileInfo(data.fileInfo)
: new FileInfo(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 FileInfo {
public name: string
public size: number
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,
})
}
-66
View File
@@ -1,66 +0,0 @@
import Data from './Data'
import { findOne } from '../lib/store'
import { ObjectId } from 'mongodb'
/**
*
* {a: {}, b: {}, ...} -> Map (a: Class, b: Class)
*
* @param Class
* @param object
*/
function mapOf(Class, object) {
return Object
.entries(object)
.reduce((map, i) => (
map.set(i[0], new Class(i[1]))
), new Map())
}
const store = () => Data.getStore('Image')
export default class Image {
static async findById(id: ObjectId) {
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: string
public file: string
public artifacts: Map<string, ImageArtifact>
constructor({ hash, file, artifacts }) {
this.hash = hash
this.file = file
this.artifacts = mapOf(ImageArtifact, artifacts)
}
getArtifact(size: string) {
return this.artifacts.get(size)
}
}
class ImageArtifact {
public url: string
public size: string
public width: number
public height: number
constructor({ url, size, width, height }) {
this.url = url
this.size = size
this.width = width
this.height = height
}
}
-105
View File
@@ -1,105 +0,0 @@
import Data from './Data'
import { find as storeFind, findOne, total } from '../lib/store'
import Image from './Image'
import { Tag } from './Tag'
import { ObjectId } from 'mongodb'
import { Breadcrumb } from '../types'
const store = () => Data.getStore('Page')
type PageDto = {
[name: string]: any
preview?: ObjectId
images: ObjectId[]
}
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 findByUrl(url: string) {
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 date: Date
public tags: Tag[]
public data: any
public images: ObjectId[]
public documents: any
public preview?: Image
public tokens: any[]
public description: string
public featured: boolean
private breadcrumb: Breadcrumb
constructor(data: { [name: string]: any }) {
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.date = data.date
this.images = data.images
this.documents = data.documents
this.preview = data.preview
this.tokens = data.tokens
this.tags = (data.tags || []).map((x: string) => new Tag(x, x))
this.featured = data.featured
this.description = data.description
}
public setBreadcrumb(value: Breadcrumb) {
this.breadcrumb = value
return this
}
public getBreadcrumb() {
return this.breadcrumb
}
public getUrl() {
return this.url
}
}
function firstItem<T>(items: T[] = []): T {
return items.length
? items[0]
: null
}
async function processPage(item: PageDto): Promise<Page> {
let preview: Image = null
const previewId = item.preview
? item.preview
: firstItem(item.images)
if (previewId) {
preview = await Image.findById(previewId)
}
return new Page({
...item,
preview,
})
}
-23
View File
@@ -1,23 +0,0 @@
import { translit } from '../lib/translit'
export class Tag {
public static fromCyrillic(tag: string) {
const slug = tag
? translit(tag).toLowerCase()
: tag
return new Tag(tag, slug)
}
public name: string
public slug: string
constructor(name: string, slug: string) {
this.name = name
this.slug = slug
}
public isValid() {
return !!this.slug
}
}
-46
View File
@@ -1,46 +0,0 @@
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: string) {
return db.collection(name)
}
export type ObjectIdLike = ObjectId | string
export function oid(i: ObjectIdLike | { _id: ObjectIdLike }): ObjectId | null {
if (i instanceof ObjectId) {
return i
} else if (typeof i === 'string') {
try {
return new ObjectId(i)
} catch (error) {
console.error(error)
return null
}
} else if (i instanceof Object) {
return '_id' in i ? oid(i._id) : null
}
return null
}
function init(db: Db) {
console.log('INIT DB')
Data.setStore('Page', db.collection('pages'))
Data.setStore('Image', db.collection('images'))
Data.setStore('Document', db.collection('documents'))
Data.setStore('Settings', db.collection('settings'))
return db
}
-38
View File
@@ -1,38 +0,0 @@
import Document from '../core/Document'
import { encodeImage, ImageDto } from './image'
export type FileDto = {
url: string
slug: string
name: string
cover?: ImageDto
filepath: string
file: {
name: string
size: number
type: string
src: string
}
}
export function encodeFile(file: Document): FileDto {
const cover = file.preview ? encodeImage(file.preview) : null
return {
url: encodeFileUrl(file),
slug: file.id,
cover,
name: file.title,
filepath: file.file,
file: {
type: 'application/pdf',
name: file.fileInfo.name,
size: file.fileInfo.size,
src: file.url,
}
}
}
export function encodeFileUrl(file: Document): string {
return `/document/${file.id}`
}
-24
View File
@@ -1,24 +0,0 @@
import Image from '../core/Image'
const ORIGINAL = 'original'
export type ImageDto = {
width: number
height: number
src: string
filepath: string
}
export function encodeImage(image: Image): ImageDto {
const a = image.getArtifact(ORIGINAL)
if (!image) {
return null
}
return {
src: a.url,
width: a.width,
height: a.height,
filepath: image.file,
}
}
-41
View File
@@ -1,41 +0,0 @@
import { encodeImage, ImageDto } from './image'
import Page from '../core/Page'
import { Breadcrumb } from '../types'
import { Tag } from '../core/Tag'
type Token = {
token: string
data: any
}
type PageDto = {
title: string
date?: string
url: string
data: string
description: string
cover?: ImageDto
tokens: Token[]
tags: Tag[]
featured: boolean
breadcrumb: Breadcrumb
}
export function encodePage(page: Page): PageDto {
const cover = page.preview ? encodeImage(page.preview) : null
const date = page.date ? page.date.toString() : null
const breadcrumb = page.getBreadcrumb()
return {
title: page.title,
url: page.url,
data: page.data,
description: page.description,
tokens: page.tokens,
tags: page.tags,
featured: page.featured,
cover,
date,
breadcrumb,
}
}
-31
View File
@@ -1,31 +0,0 @@
export function unique<T>(items: T[], key: (item: T) => string): T[] {
const map = new Map(items.map(x => [key(x), x]))
return [...map.values()]
}
export function ensureArray<T>(value: T | T[]): T[] {
if (!value) {
return []
}
if (Array.isArray(value)) {
return value
}
return [value]
}
/**
*
* Check if array has one item at least
*
* @param items
*/
export function isArrayOk<T>(items: T[]): boolean {
if (!Array.isArray(items)) {
return false
}
return items.length > 0
}
-19
View File
@@ -1,19 +0,0 @@
export async function find(collection, query, { sort, skip, limit }) {
const cursor = collection.find(query)
if (sort) cursor.sort(sort)
if (skip) cursor.skip(skip)
if (limit) cursor.limit(limit)
return cursor.toArray()
}
export async function findOne(collection, query) {
return collection.findOne(query)
}
export async function total(collection, query) {
return collection
.find(query)
.count()
}
-80
View File
@@ -1,80 +0,0 @@
const map = new Map<string, string>([
['', '_'],
['А', 'A'],
['а', 'a'],
['Б', 'B'],
['б', 'b'],
['В', 'V'],
['в', 'v'],
['Г', 'G'],
['г', 'g'],
['Д', 'D'],
['д', 'd'],
['Е', 'E'],
['е', 'e'],
['Ё', 'E'],
['ё', 'e'],
['Ж', 'Zh'],
['ж', 'zh'],
['З', 'Z'],
['з', 'z'],
['И', 'I'],
['и', 'i'],
['Й', 'Y'],
['й', 'y'],
['К', 'K'],
['к', 'k'],
['Л', 'L'],
['л', 'l'],
['М', 'M'],
['м', 'm'],
['Н', 'N'],
['н', 'n'],
['О', 'O'],
['о', 'o'],
['П', 'P'],
['п', 'p'],
['Р', 'R'],
['р', 'r'],
['С', 'S'],
['с', 's'],
['Т', 'T'],
['т', 't'],
['У', 'U'],
['у', 'u'],
['Ф', 'F'],
['ф', 'f'],
['Х', 'Kh'],
['х', 'kh'],
['Ц', 'Ts'],
['ц', 'ts'],
['Ч', 'Ch'],
['ч', 'ch'],
['Ш', 'Sh'],
['ш', 'sh'],
['Щ', 'Sch'],
['щ', 'sch'],
['ь', ''],
['Ы', 'Y'],
['ы', 'y'],
['ъ', ''],
['Э', 'E'],
['э', 'e'],
['Ю', 'Yu'],
['ю', 'yu'],
['Я', 'Ya'],
['я', 'ya'],
])
export function translit(sample: string): string {
return [...sample]
.map(char => {
if (map.has(char)) {
return map.get(char)
}
return ''
})
.join('')
}
-48
View File
@@ -1,48 +0,0 @@
import { Request } from 'express'
import { ensureArray } from './array'
export function dropTrailingSlash(path: string) {
if (path === '/' || path === '') {
return '/'
}
const lastChar = path.charAt(path.length - 1)
if (lastChar === '/') {
return path.substr(0, path.length - 1)
}
return path
}
export function getPathWaterfall(path: string): string[] {
const parts = path
.split('/')
.filter(Boolean)
.reduce((acc, part, index, parts) => {
const i = index + 1
const item = `/${parts.slice(0, i).join('/')}`
return [
...acc,
item
]
}, [])
return ['/', ...parts]
}
export function readQueryInt(req: Request, key: string) {
const int = (value: string | string[]) => parseInt(Array.isArray(value)
? value[0]
: value
)
return int(req.query[key]) || 0
}
export function readQueryString(req: Request, key: string) {
return `${req.query[key]}`
}
export function readQueryArray(req: Request, key: string) {
return ensureArray(req.query[key])
}
-42
View File
@@ -1,42 +0,0 @@
import { config } from 'dotenv'
import express from 'express'
import { connect } from './db'
import * as file from './controllers/file'
import * as page from './controllers/page'
import * as image from './controllers/image'
import * as tag from './controllers/tag'
async function main() {
const port = process.env.PORT || '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('/files/urls', file.getUrls)
app.get('/files/:slug', file.getItem)
app.get('/pages', page.getAll)
app.get('/pages/urls', page.getUrls)
app.get('/pages/tags', page.getByTags)
app.get('/page', page.getItem)
app.get('/image', image.getItem)
app.get('/tags', tag.getAll)
app.listen(port, () => {
console.log(`> Ready on http://localhost:${port}`)
})
}
config()
main()
-6
View File
@@ -1,6 +0,0 @@
export type BreadcrumbPart = {
name: string
href: string
}
export type Breadcrumb = BreadcrumbPart[]
-23
View File
@@ -1,23 +0,0 @@
{
"compilerOptions": {
"outDir": "./build",
"target": "esnext",
"lib": [
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true
},
"exclude": [
"node_modules"
],
"include": [
"**/*.ts"
]
}