diff --git a/modules/hudozka-api/.dockerignore b/modules/hudozka-api/.dockerignore new file mode 100644 index 00000000..e124c210 --- /dev/null +++ b/modules/hudozka-api/.dockerignore @@ -0,0 +1,2 @@ +.env +*.tar diff --git a/modules/hudozka-api/.gitignore b/modules/hudozka-api/.gitignore new file mode 100644 index 00000000..b8595258 --- /dev/null +++ b/modules/hudozka-api/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +*.tar diff --git a/modules/hudozka-api/Dockerfile b/modules/hudozka-api/Dockerfile new file mode 100644 index 00000000..51cf6dfc --- /dev/null +++ b/modules/hudozka-api/Dockerfile @@ -0,0 +1,27 @@ +# 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" ] diff --git a/modules/hudozka-api/package-lock.json b/modules/hudozka-api/package-lock.json new file mode 100644 index 00000000..e7300aca --- /dev/null +++ b/modules/hudozka-api/package-lock.json @@ -0,0 +1,693 @@ +{ + "name": "hudozka-api", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/bson": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.1.tgz", + "integrity": "sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/classnames": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz", + "integrity": "sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==", + "dev": true + }, + "@types/mongodb": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.3.16.tgz", + "integrity": "sha512-i1Ov36BXdp+urtPsaSvrNfCdsaVn4Ukq1m1kGyzdWB1+eg3gJ68unXU5LNmnF4EAMRFY6FXZzA7/W3NST40b8g==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "13.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.1.tgz", + "integrity": "sha512-Zq8gcQGmn4txQEJeiXo/KiLpon8TzAl0kmKH4zdWctPj05nWwp1ClMdAVEloqrQKfaC48PNLdgN/aVaLqUrluA==", + "dev": true + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + } + } + }, + "bson": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.3.tgz", + "integrity": "sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + } + } + }, + "feed": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.1.0.tgz", + "integrity": "sha512-dAXWXM8QMxZ1DRnAxDmy1MaWZFlh1Ku7TU3onbXgHrVJynsxkNGPUed1AxszVW8AXo43xExronVkIqK+ACsoBA==", + "requires": { + "xml-js": "^1.6.11" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "mongodb": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.3.tgz", + "integrity": "sha512-II7P7A3XUdPiXRgcN96qIoRa1oesM6qLNZkzfPluNZjVkgQk3jnQwOT6/uDk4USRDTTLjNFw2vwfmbRGTA7msg==", + "requires": { + "bl": "^2.2.0", + "bson": "^1.1.1", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sitemap": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-1.13.0.tgz", + "integrity": "sha1-Vpy+IYAgKSamKiZs094Jyc60P4M=", + "requires": { + "underscore": "^1.7.0", + "url-join": "^1.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "ts-node": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.6.2.tgz", + "integrity": "sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "3.1.1" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", + "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "dev": true + }, + "underscore": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz", + "integrity": "sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "url-join": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", + "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "requires": { + "sax": "^1.2.4" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/modules/hudozka-api/package.json b/modules/hudozka-api/package.json new file mode 100644 index 00000000..de0f1b08 --- /dev/null +++ b/modules/hudozka-api/package.json @@ -0,0 +1,35 @@ +{ + "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.17.1", + "feed": "^4.0.0", + "lodash": "^4.17.15", + "mongodb": "^3.5.3", + "sitemap": "^1.5.0" + }, + "devDependencies": { + "@types/classnames": "^2.2.9", + "@types/mongodb": "^3.3.16", + "@types/node": "^13.7.1", + "ts-node": "^8.6.2", + "typescript": "^3.7.5" + } +} diff --git a/modules/hudozka-api/src/controllers/album.ts b/modules/hudozka-api/src/controllers/album.ts new file mode 100644 index 00000000..4b1734e0 --- /dev/null +++ b/modules/hudozka-api/src/controllers/album.ts @@ -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` + }) + } +} diff --git a/modules/hudozka-api/src/controllers/article.ts b/modules/hudozka-api/src/controllers/article.ts new file mode 100644 index 00000000..3529591b --- /dev/null +++ b/modules/hudozka-api/src/controllers/article.ts @@ -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` + }) + } +} diff --git a/modules/hudozka-api/src/controllers/file.ts b/modules/hudozka-api/src/controllers/file.ts new file mode 100644 index 00000000..503574a8 --- /dev/null +++ b/modules/hudozka-api/src/controllers/file.ts @@ -0,0 +1,51 @@ +import { Request, Response } from 'express' +import Document from '../core/Document' +import { encodeFile } from '../factory/file' + +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` + }) + } +} diff --git a/modules/hudozka-api/src/controllers/image.ts b/modules/hudozka-api/src/controllers/image.ts new file mode 100644 index 00000000..a35b756c --- /dev/null +++ b/modules/hudozka-api/src/controllers/image.ts @@ -0,0 +1,17 @@ +import Image from '../core/Image' +import { encodeImage } from '../factory/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` + }) + } +} diff --git a/modules/hudozka-api/src/controllers/page.ts b/modules/hudozka-api/src/controllers/page.ts new file mode 100644 index 00000000..e3c835db --- /dev/null +++ b/modules/hudozka-api/src/controllers/page.ts @@ -0,0 +1,51 @@ +import { Request, Response } from 'express' +import Page from '../core/Page' +import { getPathWithNoTrailingSlash } from '../lib/url' +import { encodePage } from '../factory/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` + }) + } +} diff --git a/modules/hudozka-api/src/controllers/person.ts b/modules/hudozka-api/src/controllers/person.ts new file mode 100644 index 00000000..605947df --- /dev/null +++ b/modules/hudozka-api/src/controllers/person.ts @@ -0,0 +1,60 @@ +import Teacher from '../core/Teacher' +import { encodePerson } from '../factory/person' +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` + }) + } +} diff --git a/modules/hudozka-api/src/controllers/schedule.ts b/modules/hudozka-api/src/controllers/schedule.ts new file mode 100644 index 00000000..706bb983 --- /dev/null +++ b/modules/hudozka-api/src/controllers/schedule.ts @@ -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` + }) + } +} diff --git a/modules/hudozka-api/src/core/Album.ts b/modules/hudozka-api/src/core/Album.ts new file mode 100644 index 00000000..b34b6497 --- /dev/null +++ b/modules/hudozka-api/src/core/Album.ts @@ -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 { + 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}`, + } +} diff --git a/modules/hudozka-api/src/core/Article.ts b/modules/hudozka-api/src/core/Article.ts new file mode 100644 index 00000000..c279f325 --- /dev/null +++ b/modules/hudozka-api/src/core/Article.ts @@ -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') + +function make(C) { + return x => new C(x) +} + +export default class Article { + static async find(query, options = {}): Promise { + 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, + }) +} diff --git a/modules/hudozka-api/src/core/Config.ts b/modules/hudozka-api/src/core/Config.ts new file mode 100644 index 00000000..db3188ff --- /dev/null +++ b/modules/hudozka-api/src/core/Config.ts @@ -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) + ) + } +} diff --git a/modules/hudozka-api/src/core/Data.ts b/modules/hudozka-api/src/core/Data.ts new file mode 100644 index 00000000..8b0846d0 --- /dev/null +++ b/modules/hudozka-api/src/core/Data.ts @@ -0,0 +1,18 @@ +import { Collection } from 'mongodb' +import * as db from '../db' + +const store = new Map() + +export default class Data { + static setStore(key: string, collection: Collection) { + store.set(key, collection) + } + + static getStore(key: string): Collection { + return store.get(key) + } + + static getCollection(name: string): Collection { + return db.collection(name) + } +} diff --git a/modules/hudozka-api/src/core/Document.ts b/modules/hudozka-api/src/core/Document.ts new file mode 100644 index 00000000..7dd182b5 --- /dev/null +++ b/modules/hudozka-api/src/core/Document.ts @@ -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 { + 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, + }) +} diff --git a/modules/hudozka-api/src/core/Image.ts b/modules/hudozka-api/src/core/Image.ts new file mode 100644 index 00000000..3e1608f2 --- /dev/null +++ b/modules/hudozka-api/src/core/Image.ts @@ -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 + } +} diff --git a/modules/hudozka-api/src/core/ImageArtifactType.js b/modules/hudozka-api/src/core/ImageArtifactType.js new file mode 100644 index 00000000..360cf5d8 --- /dev/null +++ b/modules/hudozka-api/src/core/ImageArtifactType.js @@ -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 diff --git a/modules/hudozka-api/src/core/Page.ts b/modules/hudozka-api/src/core/Page.ts new file mode 100644 index 00000000..da26be7b --- /dev/null +++ b/modules/hudozka-api/src/core/Page.ts @@ -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 { + 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, + }) +} diff --git a/modules/hudozka-api/src/core/Teacher.ts b/modules/hudozka-api/src/core/Teacher.ts new file mode 100644 index 00000000..582ec73f --- /dev/null +++ b/modules/hudozka-api/src/core/Teacher.ts @@ -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 { + 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, + }) +} diff --git a/modules/hudozka-api/src/core/pages.js b/modules/hudozka-api/src/core/pages.js new file mode 100644 index 00000000..b140f10f --- /dev/null +++ b/modules/hudozka-api/src/core/pages.js @@ -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}); +} diff --git a/modules/hudozka-api/src/db.ts b/modules/hudozka-api/src/db.ts new file mode 100644 index 00000000..048e2813 --- /dev/null +++ b/modules/hudozka-api/src/db.ts @@ -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 +} diff --git a/modules/hudozka-api/src/factory/file.ts b/modules/hudozka-api/src/factory/file.ts new file mode 100644 index 00000000..1866de43 --- /dev/null +++ b/modules/hudozka-api/src/factory/file.ts @@ -0,0 +1,36 @@ +import ImageArtifactType from '../core/ImageArtifactType' + +const sizes = [ + ImageArtifactType.MEDIUM, + ImageArtifactType.BIG, + ImageArtifactType.ORIGIN, +] + +export function encodeFilePreview(file, sizes) { + const preview = file.preview + if (!preview) return null + + const image = preview.findArtifact(sizes) + if (!image) return null + + return image.url +} + +export function encodeFile(file) { + const imageUrl = encodeFilePreview(file, sizes) + const category = (file.category || '') + .replace('Documents/', '') + .replace('Pages/', '') + .replace('Albums/', '') + .replace('Articles /', '') + + return { + imageUrl, + category, + title: file.title, + fileName: file.fileInfo.name, + fileSize: file.fileInfo.size, + fileUrl: file.url, + url: file.viewUrl, + } +} diff --git a/modules/hudozka-api/src/factory/image.ts b/modules/hudozka-api/src/factory/image.ts new file mode 100644 index 00000000..717ec47b --- /dev/null +++ b/modules/hudozka-api/src/factory/image.ts @@ -0,0 +1,24 @@ +import ImageArtifactType from '../core/ImageArtifactType' + +export function encodeImage(image) { + const artifacts = [ + ImageArtifactType.SMALL, + ImageArtifactType.MEDIUM, + ImageArtifactType.BIG, + ImageArtifactType.LARGE, + ImageArtifactType.ORIGIN, + ImageArtifactType.ORIGINAL, + ImageArtifactType.FACEBOOK = 'fb' + ].reduce((acc, size) => { + return { + ...acc, + [size]: image.getPicture(size), + } + }, {}) + + return { + artifacts, + hash: image.hash, + file: image.file, + } +} diff --git a/modules/hudozka-api/src/factory/page.ts b/modules/hudozka-api/src/factory/page.ts new file mode 100644 index 00000000..ef6bb38d --- /dev/null +++ b/modules/hudozka-api/src/factory/page.ts @@ -0,0 +1,12 @@ +import { encodeImage } from './image' + +export function encodePage(page) { + return { + id: page.id, + title: page.title, + url: page.url, + data: page.data, + preview: encodeImage(page.preview), + tokens: page.tokens, + } +} diff --git a/modules/hudozka-api/src/factory/person.ts b/modules/hudozka-api/src/factory/person.ts new file mode 100644 index 00000000..21d6f410 --- /dev/null +++ b/modules/hudozka-api/src/factory/person.ts @@ -0,0 +1,13 @@ +import ImageArtifactType from '../core/ImageArtifactType' +import { encodeImage } from './image' +const profilePicture = profile => profile.picture.getPicture(ImageArtifactType.BIG) + +export function encodePerson(person) { + return { + ...person, + shortName: person.shortName(), + name: person.splitName(), + picture: profilePicture(person), + preview: encodeImage(person.picture), + } +} \ No newline at end of file diff --git a/modules/hudozka-api/src/lib/date.ts b/modules/hudozka-api/src/lib/date.ts new file mode 100644 index 00000000..4cfdd5f9 --- /dev/null +++ b/modules/hudozka-api/src/lib/date.ts @@ -0,0 +1,7 @@ +/** + * + * Date -> Unix time + * + * @param date + */ +export const timestamp = (date: Date) => date.getTime() diff --git a/modules/hudozka-api/src/lib/file.ts b/modules/hudozka-api/src/lib/file.ts new file mode 100644 index 00000000..504cc26d --- /dev/null +++ b/modules/hudozka-api/src/lib/file.ts @@ -0,0 +1,23 @@ +const getExt = (name: string) => /\.(\w+)$/.exec(name)[1] +const defaultExt = (ext: string) => ext.toUpperCase() +const exts = ['png', 'pdf', 'jpg', 'jpeg', 'gif', 'doc'] +const extd = ['ПНГ', 'ПДФ', 'ЖПГ', 'ЖПГ', 'ГИФ', 'ДОК'] +const units = ['байт', 'КБ', 'МБ', 'ГБ', 'ТБ', 'ПБ'] + +export function ext(filname: string): string { + const ext = getExt(filname) + const i = exts.indexOf(ext) + + return i > -1 + ? extd[i] + : defaultExt(ext) +} + +export function size(bytes: any, precision = 1): string { + if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '' + + const number = Math.floor(Math.log(bytes) / Math.log(1024)) + const size = (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + + return `${size} ${units[number]}` +} diff --git a/modules/hudozka-api/src/lib/sort.js b/modules/hudozka-api/src/lib/sort.js new file mode 100644 index 00000000..3fa95c00 --- /dev/null +++ b/modules/hudozka-api/src/lib/sort.js @@ -0,0 +1,37 @@ +function prioritySort(priority, key, list) { + priority = priority.reverse() + + let weight = i => { + const p = key(i) + const index = priority.indexOf(p) + return index > -1 + ? index + 1 + : 0 + } + + return sort(list, weight) +} + +function sort(list, fn, reverse = false) { + return list + .map(i => [fn(i), i]) + .sort((a, b) => reverse + ? a[0] - b[0] + : b[0] - a[0]) + .map(i => i[1]) +} + +function sortBy(fn, reverse = false) { + return (i1, i2) => { + const t1 = fn(i1) + const t2 = fn(i2) + + return reverse + ? t1 - t2 + : t2 - t1 + } +} + +exports.prioritySort = prioritySort +exports.sortBy = sortBy +exports.sort = sort diff --git a/modules/hudozka-api/src/lib/store.ts b/modules/hudozka-api/src/lib/store.ts new file mode 100644 index 00000000..309848bd --- /dev/null +++ b/modules/hudozka-api/src/lib/store.ts @@ -0,0 +1,19 @@ +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() +} diff --git a/modules/hudozka-api/src/lib/string.js b/modules/hudozka-api/src/lib/string.js new file mode 100644 index 00000000..9a8a5493 --- /dev/null +++ b/modules/hudozka-api/src/lib/string.js @@ -0,0 +1,12 @@ +function titleCase(str) { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +function trim(string) { + return string + .replace(/^\s+/, '') + .replace(/\s+$/, '') +} + +exports.titleCase = titleCase +exports.trim = trim diff --git a/modules/hudozka-api/src/lib/url.js b/modules/hudozka-api/src/lib/url.js new file mode 100644 index 00000000..d68c640d --- /dev/null +++ b/modules/hudozka-api/src/lib/url.js @@ -0,0 +1,40 @@ +function getPathWithNoTrailingSlash(path='') { + path = path.charAt(path.length - 1) === '/' + ? path.substr(0, path.length - 1) + : path + return path.length === 0 + ? '/' + : path +} + +function isEqualPath(a, b, ignoreTrailingSlash = false) { + return ignoreTrailingSlash + ? getPathWithNoTrailingSlash(a) === getPathWithNoTrailingSlash(b) + : a === b +} + +function isMatchPathPattern(pattern, path, ignoreTrailingSlash = false) { + const test = ignoreTrailingSlash + ? getPathWithNoTrailingSlash(path) + : path + return pattern.test(test) +} + +/** + * + * /article/* -> ^\/article\/[/\w\d-_]+\/?$ + * + * @param url + * @return {RegExp} + */ +function urlToPattern(url) { + const u = url.replace('/*', '[/\\w\\d-_]+') + const pattern = `^${u}/?$` + + return new RegExp(pattern) +} + +exports.getPathWithNoTrailingSlash = getPathWithNoTrailingSlash +exports.isMatchPathPattern = isMatchPathPattern +exports.urlToPattern = urlToPattern +exports.isEqualPath = isEqualPath diff --git a/modules/hudozka-api/src/models/course.js b/modules/hudozka-api/src/models/course.js new file mode 100644 index 00000000..35c33679 --- /dev/null +++ b/modules/hudozka-api/src/models/course.js @@ -0,0 +1,22 @@ +const courses = { + painting: ['Живопись', ''], + drawing: ['Рисунок', '/courses/drawing'], + sculpting: ['Скульптура', ''], + sculpture: ['Скульптура', ''], + ceramic: ['Керамика', '/courses/ceramics'], + ceramics: ['Керамика', '/courses/ceramics'], + composition: ['Композиция', ''], + 'easel-composition': ['Станковая композиция', ''], + dpi: ['ДПИ', ''], + 'art-talks': ['Беседа по искусству', ''], + history: ['История искусства', '/courses/art-history'], + cg: ['Комп. графика', '/courses/cg'], +} + +export function getCourse(id) { + const course = id in courses ? courses[id] : null + if (!course) return null + + const [title, url] = course + return {id, title, url} +} diff --git a/modules/hudozka-api/src/models/schedule.js b/modules/hudozka-api/src/models/schedule.js new file mode 100644 index 00000000..7acdde25 --- /dev/null +++ b/modules/hudozka-api/src/models/schedule.js @@ -0,0 +1,44 @@ +export const autumnMonth = 8; +export const springMonth = 0; + +export function scheduleDate(schedule){ + let {period, semester} = schedule; + + let i = semester === 'spring' ? 1 : 0; + let year = getPeriod(period)[i]; + + return new Date( + year, + i ? springMonth : autumnMonth, + 1 + ); +} + +/** + * + * f("2012-2015") -> [2012, 2015] + * f(["2012", "2015"]) -> [2012, 2015] + * f([2012, 2015]) -> [2012, 2015] + * + * @param i + * @returns {*} + */ +export function getPeriod(i){ + if (typeof i === 'string') return getPeriod(i.split('-')); + + return i.map(Number); +} + +/** + * + * f("2012-2015") -> "2012-2015" + * f(["2012", "2015"]) -> "2012-2015" + * f([2012, 2015]) -> "2012-2015" + * + * @param i + * @returns {*} + */ +export function getPeriodString(i) { + return getPeriod(i) + .join('-') +} diff --git a/modules/hudozka-api/src/server.ts b/modules/hudozka-api/src/server.ts new file mode 100644 index 00000000..53c51691 --- /dev/null +++ b/modules/hudozka-api/src/server.ts @@ -0,0 +1,57 @@ +import { config } 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' + +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}`) + }) +} + +config() +main() diff --git a/modules/hudozka-api/src/server/articles.ts b/modules/hudozka-api/src/server/articles.ts new file mode 100644 index 00000000..2dbca9f5 --- /dev/null +++ b/modules/hudozka-api/src/server/articles.ts @@ -0,0 +1,71 @@ +import Article from '../core/Article' +import ImageArtifactType from '../core/ImageArtifactType' +import { sortBy } from '../utils/sort' +import { timestamp } from '../lib/date' +import { encodeImage } from '../factory/image' + +export const sortArticleByDate = sortBy( + x => timestamp(new Date(x.date)) +) + +export async function findArticlesNin(nin, skip, limit, sort) { + const query = { + _id: { $nin: nin } + } + + return Article.find(query, { + sort, + skip, + limit, + }) +} + +export async function findArticles(page: number, pageSize: number) { + const limit = pageSize + const skip = (page - 1) * pageSize + + const pinnedArticles = page === 1 + ? await Article.findPinned() + : [] + + const total = await Article.total() + const totalPages = Math.ceil(total / pageSize) + + const id = i => i._id + const pinnedIds = pinnedArticles.map(id) + const articles = await findArticlesNin(pinnedIds, skip, limit, { date: -1 }) + + const prevPage = page > 1 + ? page - 1 + : null + + const nextPage = page < totalPages + ? page + 1 + : null + + const items = [ + ...pinnedArticles.sort(sortArticleByDate), + ...articles + ] + .map(x => x.plain()) + .map(x => ({ + ...x, + preview: !x.preview ? null : ( + x.preview.getPicture(ImageArtifactType.MEDIUM) + ) + })) + + return { + items, + prevPage, + nextPage, + totalPages, + } +} + +export function encodeArticle(article) { + return { + ...article, + preview: encodeImage(article.preview), + } +} \ No newline at end of file diff --git a/modules/hudozka-api/src/server/collective.js b/modules/hudozka-api/src/server/collective.js new file mode 100644 index 00000000..98dfb699 --- /dev/null +++ b/modules/hudozka-api/src/server/collective.js @@ -0,0 +1,18 @@ +const { collection } = require('../db') +// import { collection } from '../core/db' + +export async function getCollective(query = {}) { + return await collection('collective') + .find(query) + .toArray(); +} + +/** + * Find a person by it's id + * + * @param id + * @returns {Promise|*} + */ +export async function getPerson(id) { + return await collection('collective').findOne({ id: id }); +} diff --git a/modules/hudozka-api/src/server/lib.ts b/modules/hudozka-api/src/server/lib.ts new file mode 100644 index 00000000..898a1080 --- /dev/null +++ b/modules/hudozka-api/src/server/lib.ts @@ -0,0 +1,14 @@ +/** + * + * {a: {}, b: {}, ...} -> Map (a: Class, b: Class) + * + * @param Class + * @param object + */ +export function mapOf(Class, object) { + return Object + .entries(object) + .reduce((map, i) => ( + map.set(i[0], new Class(i[1])) + ), new Map()) +} diff --git a/modules/hudozka-api/src/server/schedule.ts b/modules/hudozka-api/src/server/schedule.ts new file mode 100644 index 00000000..f7b07a1b --- /dev/null +++ b/modules/hudozka-api/src/server/schedule.ts @@ -0,0 +1,135 @@ +import { createMap } from '../utils/common' +import { getCollective } from './collective' +import { + scheduleDate, + getPeriod as schedulePeriod, + getPeriodString +} from '../models/schedule' +import { getCourse } from '../models/course' + +// import { last } from 'lodash' +// const sortBy = require('../lib/sort').sortBy + +const SCHEDULES = 'schedules' + +export async function findSchedule(collection, period, semester) { + const schedule = await collection(SCHEDULES).findOne({ + $or: [ + { period: period }, + { period: schedulePeriod(period) } + ], + semester: semester + }) + + return schedule ? migrate(schedule) : null +} + +// async function findAllScheduleParams() { +// const schedules = await collection(SCHEDULES) +// .find({}, { +// period: 1, +// semester: 1, +// _id: 0, +// }) +// .toArray() + +// return schedules +// .map(i => ({ +// ...i, +// period: schedulePeriod(i.period), +// })) +// .sort(sortBy( +// i => scheduleDate(i).getTime(), +// true, +// )) +// } + +// function text({ semester, period }) { +// const word = semester => [['spring', 'Весна'], ['autumn', 'Осень']] +// .find(i => i[0] === semester) +// [1] +// const periodIndex = semester === 'spring' ? 1 : 0 +// const year = period[periodIndex] + +// return `${word(semester)} ${year} года` +// } + +async function migrate(schedule) { + const versions = { + '1.0': migrate10to20, + '2.0': migrate20to30, + } + + const version = schedule.version + const update = versions[version] + return version in versions ? migrate(update(schedule)) : schedule +} + +function migrate10to20(schedule) { + const dayNames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] + const time = time => time + .map(i => i.split(' ')) + + const week = days => days.reduce((week, lesson, index) => { + if (lesson) { + const day = dayNames[index] + week[day] = [Object.assign(lesson, { + time: time(lesson.time) + })] + } + + return week + }, {}) + + return Object.assign(schedule, { + version: '2.0', + period: schedulePeriod(schedule.period), + groups: schedule.schedule + .map(group => ({ + name: group.group, + time: group.time, + week: week(group.week) + })) + }) +} + +async function migrate20to30(schedule) { + const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat']//, 'sun'] + + const persons = await getCollective() + const person = createMap(i => i.id, persons) + + const week = week => days + .map(day => week[day] || []) + .map(lessons => lessons.map(lesson)) + + const lesson = i => ({ + time: i.time, + course: getCourse(i.lesson), + teacher: person.has(i.teacher) ? teacher(i.teacher) : teacherDummy(i.teacher) + }) + + const teacher = id => ({ + id: id, + url: `/teacher/${id}`, + name: personName(id) + }) + + const teacherDummy = name => ({ + id: name, + url: null, + name: name + }) + + const personName = id => person.has(id) ? person.get(id).name : null + + return Object.assign(schedule, { + version: '3.0', + groups: schedule.groups + .map(group => ({ + name: group.name, + time: group.time, + week: week(group.week) + })) + }) +} diff --git a/modules/hudozka-api/src/utils/common.ts b/modules/hudozka-api/src/utils/common.ts new file mode 100644 index 00000000..a2ec4754 --- /dev/null +++ b/modules/hudozka-api/src/utils/common.ts @@ -0,0 +1,106 @@ +/** + * Generates list of numbers + * @param {Number} start starting value + * @param {Number} num amount of numbers in list + * @param {Number} step difference value + * @return {Array} list of numbers + */ +export function range(start, num, step) { + var list = []; + var val = start; + for (var i = 0; i < num; i++) { + list.push(val); + val += step; + } + return list; +} + +/** + * Return a closure that compares a Number argument with specified Number + * @param withNumber Number to compare with + * @returns {Function} (i) => {} + */ +export function numberEquals(withNumber) { + return i => i === withNumber; +} + +/** + * Return a closure that compares a second Number argument with specified Number + * @param withNumber Number to compare with + * @returns {Function} (i) => {} + */ +export function indexEquals(withNumber) { + return (_, i) => i === withNumber; +} + +/** + * Return a reduce function + * [['spring', 'Весена'], ['autumn', 'Осень']] -> 'Весна' or 'Осень' depend on byCondition result + * @param byCondition + * @param map + */ +export function select(byCondition, map = i => i) { + return (result, item) => byCondition(item) ? map(item) : map(result) +} + +/** + * Select an item in Array by specified selector function + * @param scope + * @param selector + * @returns {*} + */ +export function choise(scope, selector) { + return scope.reduce(selector, null); +} + +export function unique(fn: (value: T) => U): (list: T[]) => U[] { + return list => Array.from( + new Set(list.map(fn)) + ) +} + +export function branch(fn) { + let keys = unique(fn); + + return list => keys(list) + .reduce((dict: any, key: any) => { + dict[key] = list.filter( + i => fn(i) === key + ); + + return dict; + }, {}) + +} + +export function assign(dict, key, value) { + dict[key] = value; + return dict; +} + +export function map(v, fn) { + return value => fn(value) ? v : value; +} + +/** + * + * f(g, [x, y, z, ...]) => map of {g(x): x, g(y): y, g(z): z} + * + * @param list + * @param selector + * @returns {*} + */ +export function createMap(selector, list) { + return list.reduce((map, i) => map.set(selector(i), i), new Map()); +} + +/** + * + * f(g) => x ? g(x) : null + * + * @param {function} fn + * @returns {function(*=): null} + */ +export function nullSafe(fn) { + return i => i ? fn(i) : null; +} diff --git a/modules/hudozka-api/src/utils/date.js b/modules/hudozka-api/src/utils/date.js new file mode 100644 index 00000000..9bdac8af --- /dev/null +++ b/modules/hudozka-api/src/utils/date.js @@ -0,0 +1,21 @@ +import {range} from './common'; + +/** + * + * @param start + * @returns {*} + */ +export function getDates(start) { + const oneDay = (1000 * 60 * 60 * 24); + const startTime = start.getTime(); + const startDay = start.getDay(); + + return range(0, 6, 1) + .reduce((dates, weekDay) => { + let delta = 1 + weekDay - startDay; + let time = startTime + (oneDay * delta); + + return dates.concat([new Date(time)]); + }, []) + .map(i => i.getDate()); +} \ No newline at end of file diff --git a/modules/hudozka-api/src/utils/fn.js b/modules/hudozka-api/src/utils/fn.js new file mode 100644 index 00000000..caaf894a --- /dev/null +++ b/modules/hudozka-api/src/utils/fn.js @@ -0,0 +1,13 @@ +/** + * + * f([x, y, z, ...]) -> f(v) -> {v.x, v.y, v.z} + * + * @param fields + * @returns {function(*): *} + */ +export function select(fields=[]){ + return i => fields.reduce((out, key) => { + out[key] = i[key]; + return out; + }, {}); +} diff --git a/modules/hudozka-api/src/utils/net.js b/modules/hudozka-api/src/utils/net.js new file mode 100644 index 00000000..623cab68 --- /dev/null +++ b/modules/hudozka-api/src/utils/net.js @@ -0,0 +1,8 @@ +export function query(list) { + return list + .reduce( + (query, param) => query.concat([`${param[0]}=${param[1]}`]), + [] + ) + .join('&'); +} diff --git a/modules/hudozka-api/src/utils/populate.js b/modules/hudozka-api/src/utils/populate.js new file mode 100644 index 00000000..d5a50261 --- /dev/null +++ b/modules/hudozka-api/src/utils/populate.js @@ -0,0 +1,35 @@ +export function pop(data){ + var Populator = function(data){ + this.target = data; + } + + Populator.prototype = { + populate: function(p){ + var self = this; + if(typeof p == "function"){ + self.target = p(self.target); + }else if(p instanceof Array){ + p.forEach(function(i){ + self.target = self.populate(i).result(); + }); + + } + return self; + }, + result: function(){ + return this.target; + } + }; + + var fun = new Populator(data); + return fun; +} + +export function populate(fn, key){ + return (dict) => { + let id = dict[key]; + let value = fn(id); + dict[key] = value ? value : id; + return dict; + } +} \ No newline at end of file diff --git a/modules/hudozka-api/src/utils/sort.js b/modules/hudozka-api/src/utils/sort.js new file mode 100644 index 00000000..774b1adf --- /dev/null +++ b/modules/hudozka-api/src/utils/sort.js @@ -0,0 +1,26 @@ +export function sortByPattern(list, pattern, map) { + let weight = i => { + const index = pattern.indexOf(map(i)) + return index > -1 ? pattern.length - index : 0 + } + + return sortByWeight(list, weight) +} + +export function sortByWeight(list, fn) { + return list + .map(i => [fn(i), i]) + .sort((a, b) => b[0] - a[0]) + .map(i => i[1]) +} + +export function sortBy(fn) { + return (i1, i2) => { + let t1 = fn(i1) + let t2 = fn(i2) + + if (t1 < t2) return 1 + if (t1 > t2) return -1 + return 0 + } +} diff --git a/modules/hudozka-api/src/utils/string.js b/modules/hudozka-api/src/utils/string.js new file mode 100644 index 00000000..a5e7dace --- /dev/null +++ b/modules/hudozka-api/src/utils/string.js @@ -0,0 +1,22 @@ +import {map} from './common'; + +/** + * Return a function that accept one String parameter and returns result of replacing. + * + * @param searchValue + * @param replaceValue + */ +export const stringReplace = (searchValue, replaceValue) => + str => str.replace(searchValue, replaceValue); + +let toTrue = map(true, i => i === 'true'); +let toFalse = map(false, i => i === 'false'); +const boolProcessors = [toTrue, toFalse]; + +export function stringToBoolean(string){ + return boolProcessors + .reduce( + (value, fn) => fn(value), + string + ); +} \ No newline at end of file diff --git a/modules/hudozka-api/tsconfig.json b/modules/hudozka-api/tsconfig.json new file mode 100644 index 00000000..c0262a52 --- /dev/null +++ b/modules/hudozka-api/tsconfig.json @@ -0,0 +1,23 @@ +{ + "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" + ] +}