Add intro/part1
6
intro/server/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
coverage/
|
||||
npm-debug.log
|
||||
stats.json
|
||||
yarn-error.log
|
||||
21
intro/server/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Ice Services
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
6
intro/server/data/articles.db
Normal file
@@ -0,0 +1,6 @@
|
||||
{"title":"this is my title","description":"this is just a test","body":"Hello World!","tagList":[],"slug":"this-is-my-title-ladzo0","author":"gsZdyqSGbQscoIU6","createdAt":{"$$date":1525546112100},"updatedAt":{"$$date":1525546112100},"_id":"9kCKBGvHDiWaDSjj"}
|
||||
{"title":"Elm is fun!","description":"Programming","body":"I've really been enjoying it!","tagList":["elm","fun"],"slug":"elm-is-fun--zb6nba","author":"Z1YiwpVIz2GQQ13Q","createdAt":{"$$date":1525523441276},"updatedAt":{"$$date":1525523455652},"_id":"AfnCDbXe6wi8Vg8C"}
|
||||
{"title":"Who says undefined isn't a function anyway?","description":"Functions","body":"Quite frankly I think undefined can be anything it wants to be, if it believes in itself.","tagList":["programming"],"slug":"who-says-undefined-isnt-a-function-anyway-t39ope","author":"Z1YiwpVIz2GQQ13Q","createdAt":{"$$date":1525523547620},"updatedAt":{"$$date":1525523547620},"_id":"CDCDlBclmwWpWdCX"}
|
||||
{"title":"Are dragons real?","description":"dragons","body":"Do Komodo Dragons count? I think they should. It's right there in the name!","tagList":["dragons"],"slug":"are-dragons-real-467lsh","author":"Z1YiwpVIz2GQQ13Q","createdAt":{"$$date":1525523750396},"updatedAt":{"$$date":1525523750396},"_id":"I2h7s1VuXciCP5nl"}
|
||||
{"title":"Hello World","description":"stuff","body":"this is not port 8000","tagList":[],"slug":"hello-world-dwkkns","author":"gsZdyqSGbQscoIU6","createdAt":{"$$date":1525547607765},"updatedAt":{"$$date":1525547607765},"_id":"fiusiOt77z4zbKe8"}
|
||||
{"title":"This compiler is pretty neat","description":"Elm","body":"It tells me about problems in my code. How neat is that?","tagList":["compilers","elm"],"slug":"this-compiler-is-pretty-neat-9ycui8","author":"Z1YiwpVIz2GQQ13Q","createdAt":{"$$date":1525523694805},"updatedAt":{"$$date":1525523694805},"_id":"ttAbrJu6OrOJ7jAf"}
|
||||
0
intro/server/data/comments.db
Normal file
0
intro/server/data/favorites.db
Normal file
1
intro/server/data/follows.db
Normal file
@@ -0,0 +1 @@
|
||||
{"follow":"Z1YiwpVIz2GQQ13Q","user":"gsZdyqSGbQscoIU6","createdAt":{"$$date":1525550850919},"_id":"f8Yep9EguoPa8Rwn"}
|
||||
3
intro/server/data/users.db
Normal file
@@ -0,0 +1,3 @@
|
||||
{"username":"rtfeldman","email":"richard.t.feldman@gmail.com","password":"$2a$10$4WPM2GSk3pH8COW1UVAVEeu2OC7/CrCzjR91PcPZIw/Ycdvyg3Aqi","bio":"","image":null,"createdAt":{"$$date":1533459071196},"_id":"7r8Pdo5csPGElToj"}
|
||||
{"username":"SamSample","email":"sam@sample.com","password":"samsample","bio":"I'm the sample user for the workshop. Hi!","image":"https://user-images.githubusercontent.com/1094080/39663282-6459c64e-503e-11e8-8da8-a2af2c81d052.png","createdAt":{"$$date":1525523183044},"_id":"Z1YiwpVIz2GQQ13Q","updatedAt":{"$$date":1525523378471}}
|
||||
{"username":"wefjbsfdjkhdgrskjhsdfgjhb","email":"sdfgjhsgjhbdfgkjhsdf@sfgdhjfjhgdfgf.com","password":"$2a$10$gnFdddybmitDP.yKM3OjfubgZ1O3geK9N8LymFV4mZYaSGe9WYPby","bio":"","image":null,"createdAt":{"$$date":1525546056406},"_id":"gsZdyqSGbQscoIU6"}
|
||||
32
intro/server/elm.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.0",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"NoRedInk/json-decode-pipeline": "1.0.0",
|
||||
"elm/browser": "1.0.0",
|
||||
"elm/core": "1.0.0",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "1.0.0",
|
||||
"elm/json": "1.0.0",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"elm-explorations/markdown": "1.0.0",
|
||||
"lukewestby/elm-http-builder": "6.0.0",
|
||||
"rtfeldman/elm-iso8601": "1.0.1",
|
||||
"rtfeldman/elm-validate": "4.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/parser": "1.0.0",
|
||||
"elm/regex": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.0"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
||||
BIN
intro/server/logo.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
31
intro/server/mixins/db.mixin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("path");
|
||||
const mkdir = require("mkdirp").sync;
|
||||
|
||||
const DbService = require("moleculer-db");
|
||||
|
||||
//process.env.MONGO_URI = "mongodb://localhost/conduit";
|
||||
|
||||
module.exports = function(collection) {
|
||||
if (process.env.MONGO_URI) {
|
||||
// Mongo adapter
|
||||
const MongoAdapter = require("moleculer-db-adapter-mongo");
|
||||
|
||||
return {
|
||||
mixins: [DbService],
|
||||
adapter: new MongoAdapter(process.env.MONGO_URI),
|
||||
collection
|
||||
};
|
||||
}
|
||||
|
||||
// --- NeDB fallback DB adapter
|
||||
|
||||
// Create data folder
|
||||
mkdir(path.resolve("./data"));
|
||||
|
||||
return {
|
||||
mixins: [DbService],
|
||||
adapter: new DbService.MemoryAdapter({ filename: `./data/${collection}.db` })
|
||||
};
|
||||
};
|
||||
15
intro/server/moleculer.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
|
||||
const os = require("os");
|
||||
|
||||
module.exports = {
|
||||
// It will be unique when scale up instances in Docker or on local computer
|
||||
nodeID: os.hostname().toLowerCase() + "-" + process.pid,
|
||||
|
||||
logger: false,
|
||||
logLevel: "info",
|
||||
|
||||
cacher: "memory",
|
||||
|
||||
metrics: false
|
||||
};
|
||||
8509
intro/server/package-lock.json
generated
Normal file
47
intro/server/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "moleculer-realworld-example-app",
|
||||
"version": "1.0.0",
|
||||
"description": "RealWorld example app with Moleculer microservices framework",
|
||||
"scripts": {
|
||||
"start": "moleculer-runner services"
|
||||
},
|
||||
"keywords": [
|
||||
"microservices",
|
||||
"moleculer",
|
||||
"realworld"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"eslint": "4.11.0",
|
||||
"jest": "21.2.1",
|
||||
"jest-cli": "21.2.1",
|
||||
"moleculer-repl": "^0.3.0",
|
||||
"npm-check": "5.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"jsonwebtoken": "8.1.0",
|
||||
"lodash": "4.17.4",
|
||||
"moleculer": "^0.11.0",
|
||||
"moleculer-db": "0.7.0",
|
||||
"moleculer-db-adapter-mongo": "0.1.6",
|
||||
"moleculer-web": "0.6.0-beta7",
|
||||
"nats": "0.7.24",
|
||||
"slug": "0.9.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ice-services/moleculer-realworld-example-app.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.x.x"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"rootDir": "./services",
|
||||
"roots": [
|
||||
"../test"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
intro/server/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
intro/server/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
intro/server/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
4
intro/server/public/asset-manifest.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"main.js": "static/js/main.cd99036f.js",
|
||||
"main.js.map": "static/js/main.cd99036f.js.map"
|
||||
}
|
||||
BIN
intro/server/public/assets/images/error.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
intro/server/public/assets/images/smiley-cyrus.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
9
intro/server/public/browserconfig.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
BIN
intro/server/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 736 B |
BIN
intro/server/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 985 B |
BIN
intro/server/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
528
intro/server/public/fonts.css
Normal file
@@ -0,0 +1,528 @@
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Merriweather Sans'), local('MerriweatherSans-Regular'), url(http://fonts.gstatic.com/s/merriweathersans/v9/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX7QjXp8Bte9ZM.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Merriweather Sans'), local('MerriweatherSans-Regular'), url(http://fonts.gstatic.com/s/merriweathersans/v9/2-c99IRs1JiJN1FRAMjTN5zd9vgsFHX1QjXp8Bte.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Merriweather Sans Bold'), local('MerriweatherSans-Bold'), url(http://fonts.gstatic.com/s/merriweathersans/v9/2-c49IRs1JiJN1FRAMjTN5zd9vgsFH1OZyDK0hZ0z5qZUqw.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Merriweather Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Merriweather Sans Bold'), local('MerriweatherSans-Bold'), url(http://fonts.gstatic.com/s/merriweathersans/v9/2-c49IRs1JiJN1FRAMjTN5zd9vgsFH1OZyDE0hZ0z5qZ.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidh18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkido18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidg18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidv18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidj18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkidi18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZMkids18S0xR41.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7qsDJB9cme_xc.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7jsDJB9cme_xc.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7rsDJB9cme_xc.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7ksDJB9cme_xc.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7osDJB9cme_xc.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7psDJB9cme_xc.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK1dSBYKcSV-LCoeQqfX1RYOo3qPZ7nsDJB9cme.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdh18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdo18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdg18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdv18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdj18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCdi18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZY4lCds18S0xR41.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdh18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdo18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdg18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdv18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdj18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSdi18S0xR41YDw.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold Italic'), local('SourceSansPro-BoldItalic'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKwdSBYKcSV-LCoeQqfX1RYOo3qPZZclSds18S0xR41.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmhdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwkxdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmxdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlBdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmBdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwmRdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ik4zwlxdu3cOWxw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNa7lujVj9_mf.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qPK7lujVj9_mf.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNK7lujVj9_mf.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qO67lujVj9_mf.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qN67lujVj9_mf.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qNq7lujVj9_mf.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7lujVj9w.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmhdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwkxdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmxdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlBdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmBdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwmRdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3i54rwlxdu3cOWxw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmhdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwkxdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
/* greek-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmxdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
/* greek */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0370-03FF;
|
||||
}
|
||||
/* vietnamese */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0102-0103, U+0110-0111, U+1EA0-1EF9, U+20AB;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRdu3cOWxy40.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(http://fonts.gstatic.com/s/sourcesanspro/v11/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu3cOWxw.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Serif Pro'), local('SourceSerifPro-Regular'), url(http://fonts.gstatic.com/s/sourceserifpro/v5/neIQzD-0qpwxpaWvjeD0X88SAOeauXo-oAGIyY0Wfw.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Source Serif Pro'), local('SourceSerifPro-Regular'), url(http://fonts.gstatic.com/s/sourceserifpro/v5/neIQzD-0qpwxpaWvjeD0X88SAOeauXQ-oAGIyY0.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Serif Pro Bold'), local('SourceSerifPro-Bold'), url(http://fonts.gstatic.com/s/sourceserifpro/v5/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSKqxKcsdrOPbQ.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Source Serif Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Source Serif Pro Bold'), local('SourceSerifPro-Bold'), url(http://fonts.gstatic.com/s/sourceserifpro/v5/neIXzD-0qpwxpaWvjeD0X88SAOeasc8btSyqxKcsdrM.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Titillium Web';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Titillium Web Bold'), local('TitilliumWeb-Bold'), url(http://fonts.gstatic.com/s/titilliumweb/v6/NaPDcZTIAOhVxoMyOr9n_E7ffHjDGIVzY5abuWIGxA.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Titillium Web';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Titillium Web Bold'), local('TitilliumWeb-Bold'), url(http://fonts.gstatic.com/s/titilliumweb/v6/NaPDcZTIAOhVxoMyOr9n_E7ffHjDGItzY5abuWI.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
BIN
intro/server/public/images/smiley-cyrus.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
43
intro/server/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Elm Workshop</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
|
||||
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
|
||||
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css">
|
||||
<link href="/fonts.css" rel="stylesheet" type="text/css">
|
||||
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
<style>/* Loading spinner courtesy of https://github.com/tobiasahlin/SpinKit */
|
||||
.sk-three-bounce { margin-right: 10px; } .sk-three-bounce .sk-child { width: 14px; height: 14px; background-color: #333; border-radius: 100%; display: inline-block; -webkit-animation: sk-three-bounce 1.4s ease-in-out 0s infinite both; animation: sk-three-bounce 1.4s ease-in-out 0s infinite both; } .sk-three-bounce .sk-bounce1 { -webkit-animation-delay: 0.28s; animation-delay: 0.28s; } .sk-three-bounce .sk-bounce2 { -webkit-animation-delay: 0.44s; animation-delay: 0.44s; } .sk-three-bounce .sk-bounce3 { -webkit-animation-delay: 0.6s; animation-delay: 0.6s; } @-webkit-keyframes sk-three-bounce { 0%, 80%, 100% { -webkit-transform: scale(0); transform: scale(0); } 40% { -webkit-transform: scale(1); transform: scale(1); } } @keyframes sk-three-bounce { 0%, 80%, 100% { -webkit-transform: scale(0); transform: scale(0); } 40% { -webkit-transform: scale(1); transform: scale(1); } }
|
||||
</style>
|
||||
|
||||
|
||||
<script src="/elm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
var app = Elm.Main.init({flags: localStorage.session || null});
|
||||
|
||||
app.ports.storeSession.subscribe(function(session) {
|
||||
localStorage.session = session;
|
||||
});
|
||||
|
||||
window.addEventListener("storage", function(event) {
|
||||
if (event.storageArea === localStorage && event.key === "session") {
|
||||
app.ports.onSessionChange.send(event.newValue);
|
||||
}
|
||||
}, false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
4
intro/server/public/main.css
Normal file
BIN
intro/server/public/mstile-144x144.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
intro/server/public/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
intro/server/public/mstile-310x150.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
intro/server/public/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
intro/server/public/mstile-70x70.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
29
intro/server/public/safari-pinned-tab.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M235 6959 c22 -22 365 -365 761 -762 l721 -722 1652 0 c908 -1 1651
|
||||
2 1651 5 0 3 -341 346 -758 763 l-757 757 -1655 0 -1654 0 39 -41z"/>
|
||||
<path d="M3900 6995 c0 -3 698 -704 1550 -1557 l1550 -1551 0 1557 0 1556
|
||||
-1550 0 c-852 0 -1550 -2 -1550 -5z"/>
|
||||
<path d="M0 3495 l0 -3310 1655 1655 c909 910 1653 1655 1652 1657 -5 5 -3235
|
||||
3237 -3269 3270 l-38 37 0 -3309z"/>
|
||||
<path d="M2008 5199 c-5 -3 329 -345 743 -758 l752 -752 753 753 c414 414 751
|
||||
755 748 757 -6 7 -2985 7 -2996 0z"/>
|
||||
<path d="M4522 4327 c-452 -453 -822 -827 -822 -831 0 -4 369 -376 821 -828
|
||||
l821 -821 829 829 829 829 -822 822 c-453 453 -825 823 -828 823 -3 0 -375
|
||||
-370 -828 -823z"/>
|
||||
<path d="M1853 1655 c-904 -905 -1643 -1647 -1643 -1650 0 -3 1484 -5 3297 -5
|
||||
l3298 0 -1650 1650 c-907 908 -1652 1650 -1655 1650 -3 0 -744 -741 -1647
|
||||
-1645z"/>
|
||||
<path d="M6265 2384 c-396 -398 -722 -726 -724 -728 -2 -3 326 -335 728 -737
|
||||
l731 -732 0 1461 c0 804 -3 1462 -7 1461 -5 -1 -332 -327 -728 -725z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
19
intro/server/public/site.webmanifest
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
intro/server/rw-logo.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
171
intro/server/services/api.service.js
Normal file
@@ -0,0 +1,171 @@
|
||||
"use strict";
|
||||
|
||||
const _ = require("lodash");
|
||||
const ApiGateway = require("moleculer-web");
|
||||
const { UnAuthorizedError } = ApiGateway.Errors;
|
||||
|
||||
module.exports = {
|
||||
name: "api",
|
||||
mixins: [ApiGateway],
|
||||
|
||||
settings: {
|
||||
port: process.env.PORT || 3000,
|
||||
|
||||
routes: [{
|
||||
path: "/api",
|
||||
|
||||
authorization: true,
|
||||
|
||||
aliases: {
|
||||
// Login
|
||||
"POST /users/login": "users.login",
|
||||
|
||||
// Users
|
||||
"REST /users": "users",
|
||||
|
||||
// Current user
|
||||
"GET /user": "users.me",
|
||||
"PUT /user": "users.updateMyself",
|
||||
|
||||
// Articles
|
||||
"GET /articles/feed": "articles.feed",
|
||||
"REST /articles": "articles",
|
||||
"GET /tags": "articles.tags",
|
||||
|
||||
// Comments
|
||||
"GET /articles/:slug/comments": "articles.comments",
|
||||
"POST /articles/:slug/comments": "articles.addComment",
|
||||
"PUT /articles/:slug/comments/:commentID": "articles.updateComment",
|
||||
"DELETE /articles/:slug/comments/:commentID": "articles.removeComment",
|
||||
|
||||
// Favorites
|
||||
"POST /articles/:slug/favorite": "articles.favorite",
|
||||
"DELETE /articles/:slug/favorite": "articles.unfavorite",
|
||||
|
||||
// Profile
|
||||
"GET /profiles/:username": "users.profile",
|
||||
"POST /profiles/:username/follow": "users.follow",
|
||||
"DELETE /profiles/:username/follow": "users.unfollow",
|
||||
},
|
||||
|
||||
// Disable to call not-mapped actions
|
||||
mappingPolicy: "restrict",
|
||||
|
||||
// Set CORS headers
|
||||
cors: true,
|
||||
|
||||
// Parse body content
|
||||
bodyParsers: {
|
||||
json: {
|
||||
strict: false
|
||||
},
|
||||
urlencoded: {
|
||||
extended: false
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
||||
assets: {
|
||||
folder: "./public"
|
||||
},
|
||||
|
||||
// logRequestParams: "info",
|
||||
// logResponseData: "info",
|
||||
|
||||
onError(req, res, err) {
|
||||
// Return with the error as JSON object
|
||||
res.setHeader("Content-type", "application/json; charset=utf-8");
|
||||
res.writeHead(err.code || 500);
|
||||
|
||||
if (err.code == 422) {
|
||||
let o = {};
|
||||
err.data.forEach(e => {
|
||||
let field = e.field.split(".").pop();
|
||||
o[field] = e.message;
|
||||
});
|
||||
|
||||
res.end(JSON.stringify({ errors: o }, null, 2));
|
||||
} else {
|
||||
const errObj = _.pick(err, ["name", "message", "code", "type", "data"]);
|
||||
res.end(JSON.stringify(errObj, null, 2));
|
||||
}
|
||||
this.logResponse(req, res, err? err.ctx : null);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Authorize the request
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} route
|
||||
* @param {IncomingRequest} req
|
||||
* @returns {Promise}
|
||||
*/
|
||||
authorize(ctx, route, req) {
|
||||
let token;
|
||||
if (req.headers.authorization) {
|
||||
let type = req.headers.authorization.split(" ")[0];
|
||||
if (type === "Token" || type === "Bearer")
|
||||
token = req.headers.authorization.split(" ")[1];
|
||||
}
|
||||
|
||||
return this.Promise.resolve(token)
|
||||
.then(token => {
|
||||
if (token) {
|
||||
// Verify JWT token
|
||||
return ctx.call("users.resolveToken", { token })
|
||||
.then(user => {
|
||||
if (user) {
|
||||
this.logger.info("Authenticated via JWT: ", user.username);
|
||||
// Reduce user fields (it will be transferred to other nodes)
|
||||
ctx.meta.user = _.pick(user, ["_id", "username", "email", "image"]);
|
||||
ctx.meta.token = token;
|
||||
}
|
||||
return user;
|
||||
})
|
||||
.catch(err => {
|
||||
// Ignored because we continue processing if user is not exist
|
||||
return null;
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(user => {
|
||||
if (req.$endpoint.action.auth == "required" && !user)
|
||||
return this.Promise.reject(new UnAuthorizedError());
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert ValidationError to RealWorld.io result
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
* @param {*} err
|
||||
*/
|
||||
/*sendError(req, res, err) {
|
||||
if (err.code == 422) {
|
||||
res.setHeader("Content-type", "application/json; charset=utf-8");
|
||||
res.writeHead(422);
|
||||
let o = {};
|
||||
err.data.forEach(e => {
|
||||
let field = e.field.split(".").pop();
|
||||
o[field] = e.message;
|
||||
});
|
||||
return res.end(JSON.stringify({
|
||||
errors: o
|
||||
}, null, 2));
|
||||
|
||||
}
|
||||
|
||||
return this._sendError(req, res, err);
|
||||
}*/
|
||||
},
|
||||
|
||||
created() {
|
||||
// Pointer to the original function
|
||||
//this._sendError = ApiGateway.methods.sendError.bind(this);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
614
intro/server/services/articles.service.js
Normal file
@@ -0,0 +1,614 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
const { ForbiddenError } = require("moleculer-web").Errors;
|
||||
|
||||
const _ = require("lodash");
|
||||
const slug = require("slug");
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "articles",
|
||||
mixins: [DbService("articles")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
fields: ["_id", "title", "slug", "description", "body", "tagList", "createdAt", "updatedAt", "favorited", "favoritesCount", "author", "comments"],
|
||||
|
||||
// Populates
|
||||
populates: {
|
||||
author: {
|
||||
action: "users.get",
|
||||
params: {
|
||||
fields: ["username", "bio", "image"]
|
||||
}
|
||||
},
|
||||
comments: {
|
||||
action: "comments.get",
|
||||
params: {
|
||||
fields: ["_id", "body", "author"],
|
||||
populates: ["author"]
|
||||
}
|
||||
},
|
||||
favorited(ids, articles, rule, ctx) {
|
||||
if (ctx.meta.user)
|
||||
return this.Promise.all(articles.map(article => ctx.call("favorites.has", { article: article._id.toString(), user: ctx.meta.user._id.toString() }).then(res => article.favorited = res)));
|
||||
else {
|
||||
articles.forEach(article => article.favorited = false);
|
||||
return this.Promise.resolve();
|
||||
}
|
||||
},
|
||||
favoritesCount(ids, articles, rule, ctx) {
|
||||
return this.Promise.all(articles.map(article => ctx.call("favorites.count", { article: article._id.toString() }).then(count => article.favoritesCount = count)));
|
||||
}
|
||||
},
|
||||
|
||||
// Validation schema for new entities
|
||||
entityValidator: {
|
||||
title: { type: "string", min: 1 },
|
||||
description: { type: "string", min: 1 },
|
||||
body: { type: "string", min: 1 },
|
||||
tagList: { type: "array", items: "string", optional: true },
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Create a new article.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {Object} article - Article entity
|
||||
*
|
||||
* @returns {Object} Created entity
|
||||
*/
|
||||
create: {
|
||||
auth: "required",
|
||||
params: {
|
||||
article: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
let entity = ctx.params.article;
|
||||
return this.validateEntity(entity)
|
||||
.then(() => {
|
||||
|
||||
entity.slug = slug(entity.title, { lower: true }) + "-" + (Math.random() * Math.pow(36, 6) | 0).toString(36);
|
||||
entity.author = ctx.meta.user._id.toString();
|
||||
entity.createdAt = new Date();
|
||||
entity.updatedAt = new Date();
|
||||
|
||||
return this.adapter.insert(entity)
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an article.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article ID
|
||||
* @param {Object} article - Article modified fields
|
||||
*
|
||||
* @returns {Object} Updated entity
|
||||
*/
|
||||
update: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "string" },
|
||||
article: { type: "object", props: {
|
||||
title: { type: "string", min: 1, optional: true },
|
||||
description: { type: "string", min: 1, optional: true },
|
||||
body: { type: "string", min: 1, optional: true },
|
||||
tagList: { type: "array", items: "string", optional: true },
|
||||
} }
|
||||
},
|
||||
handler(ctx) {
|
||||
let newData = ctx.params.article;
|
||||
newData.updatedAt = new Date();
|
||||
// the 'id' is the slug
|
||||
return this.Promise.resolve(ctx.params.id)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
if (article.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
const update = {
|
||||
"$set": newData
|
||||
};
|
||||
|
||||
return this.adapter.updateById(article._id, update);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List articles with pagination.
|
||||
*
|
||||
* @actions
|
||||
* @param {String} tag - Filter for 'tag'
|
||||
* @param {String} author - Filter for author ID
|
||||
* @param {String} favorited - Filter for favorited author
|
||||
* @param {Number} limit - Pagination limit
|
||||
* @param {Number} offset - Pagination offset
|
||||
*
|
||||
* @returns {Object} List of articles
|
||||
*/
|
||||
list: {
|
||||
cache: {
|
||||
keys: ["#token", "tag", "author", "favorited", "limit", "offset"]
|
||||
},
|
||||
params: {
|
||||
tag: { type: "string", optional: true },
|
||||
author: { type: "string", optional: true },
|
||||
favorited: { type: "string", optional: true },
|
||||
limit: { type: "number", optional: true, convert: true },
|
||||
offset: { type: "number", optional: true, convert: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
const limit = ctx.params.limit ? Number(ctx.params.limit) : 20;
|
||||
const offset = ctx.params.offset ? Number(ctx.params.offset) : 0;
|
||||
|
||||
let params = {
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-createdAt"],
|
||||
populate: ["author", "favorited", "favoritesCount"],
|
||||
query: {}
|
||||
};
|
||||
let countParams;
|
||||
|
||||
if (ctx.params.tag)
|
||||
params.query.tagList = {"$in" : [ctx.params.tag]};
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
if (ctx.params.author) {
|
||||
return ctx.call("users.find", { query: { username: ctx.params.author } })
|
||||
.then(users => {
|
||||
if (users.length == 0)
|
||||
return this.Promise.reject(new MoleculerClientError("Author not found"));
|
||||
|
||||
params.query.author = users[0]._id;
|
||||
});
|
||||
}
|
||||
if (ctx.params.favorited) {
|
||||
return ctx.call("users.find", { query: { username: ctx.params.favorited } })
|
||||
.then(users => {
|
||||
if (users.length == 0)
|
||||
return this.Promise.reject(new MoleculerClientError("Author not found"));
|
||||
|
||||
return users[0]._id;
|
||||
})
|
||||
.then(user => {
|
||||
return ctx.call("favorites.find", { fields: ["article"], query: { user }})
|
||||
.then(list => {
|
||||
params.query._id = { $in: list.map(o => o.article) };
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
countParams = Object.assign({}, params);
|
||||
// Remove pagination params
|
||||
if (countParams && countParams.limit)
|
||||
countParams.limit = null;
|
||||
if (countParams && countParams.offset)
|
||||
countParams.offset = null;
|
||||
})
|
||||
.then(() => this.Promise.all([
|
||||
// Get rows
|
||||
this.adapter.find(params),
|
||||
|
||||
// Get count of all rows
|
||||
this.adapter.count(countParams)
|
||||
|
||||
])).then(res => {
|
||||
return this.transformDocuments(ctx, params, res[0])
|
||||
.then(docs => this.transformResult(ctx, docs, ctx.meta.user))
|
||||
.then(r => {
|
||||
r.articlesCount = res[1];
|
||||
return r;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List articles from followed authors.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {Number} limit - Pagination limit
|
||||
* @param {Number} offset - Pagination offset
|
||||
*
|
||||
* @returns {Object} List of articles
|
||||
*/
|
||||
feed: {
|
||||
auth: "required",
|
||||
cache: {
|
||||
keys: ["#token", "limit", "offset"]
|
||||
},
|
||||
params: {
|
||||
limit: { type: "number", optional: true, convert: true },
|
||||
offset: { type: "number", optional: true, convert: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
const limit = ctx.params.limit ? Number(ctx.params.limit) : 20;
|
||||
const offset = ctx.params.offset ? Number(ctx.params.offset) : 0;
|
||||
|
||||
let params = {
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-createdAt"],
|
||||
populate: ["author", "favorited", "favoritesCount"],
|
||||
query: {}
|
||||
};
|
||||
let countParams;
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
return ctx.call("follows.find", { fields: ["follow"], query: { user: ctx.meta.user._id.toString() } })
|
||||
.then(list => {
|
||||
const authors = _.uniq(_.compact(_.flattenDeep(list.map(o => o.follow))));
|
||||
params.query.author = {"$in" : authors};
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
countParams = Object.assign({}, params);
|
||||
// Remove pagination params
|
||||
if (countParams && countParams.limit)
|
||||
countParams.limit = null;
|
||||
if (countParams && countParams.offset)
|
||||
countParams.offset = null;
|
||||
})
|
||||
.then(() => this.Promise.all([
|
||||
// Get rows
|
||||
this.adapter.find(params),
|
||||
|
||||
// Get count of all rows
|
||||
this.adapter.count(countParams)
|
||||
|
||||
])).then(res => {
|
||||
return this.transformDocuments(ctx, params, res[0])
|
||||
.then(docs => this.transformResult(ctx, docs, ctx.meta.user))
|
||||
.then(r => {
|
||||
r.articlesCount = res[1];
|
||||
return r;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an article by slug
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Object} Article entity
|
||||
*/
|
||||
get: {
|
||||
cache: {
|
||||
keys: ["#token", "id"]
|
||||
},
|
||||
params: {
|
||||
id: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.findBySlug(ctx.params.id)
|
||||
.then(entity => {
|
||||
if (!entity)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found!", 404));
|
||||
|
||||
return entity;
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"] }, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove an article by slug
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Number} Count of removed articles
|
||||
*/
|
||||
remove: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "any" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.findBySlug(ctx.params.id)
|
||||
.then(entity => {
|
||||
if (!entity)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found!", 404));
|
||||
|
||||
if (entity.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
return this.adapter.removeById(entity._id)
|
||||
.then(() => ctx.call("favorites.removeByArticle", { article: entity._id }))
|
||||
.then(json => this.entityChanged("removed", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Favorite an article
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Object} Updated article
|
||||
*/
|
||||
favorite: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("favorites.add", { article: article._id.toString(), user: ctx.meta.user._id.toString() }).then(() => article);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"] }, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unfavorite an article
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Article slug
|
||||
*
|
||||
* @returns {Object} Updated article
|
||||
*/
|
||||
unfavorite: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("favorites.delete", { article: article._id.toString(), user: ctx.meta.user._id.toString() }).then(() => article);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author", "favorited", "favoritesCount"] }, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get list of available tags
|
||||
*
|
||||
* @returns {Object} Tag list
|
||||
*/
|
||||
tags: {
|
||||
cache: {
|
||||
keys: []
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve()
|
||||
.then(() => this.adapter.find({ fields: ["tagList"], sort: ["createdAt"] }))
|
||||
.then(list => {
|
||||
return _.uniq(_.compact(_.flattenDeep(list.map(o => o.tagList))));
|
||||
})
|
||||
.then(tags => ({ tags }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all comments of an article.
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
*
|
||||
* @returns {Object} Comment list
|
||||
*
|
||||
*/
|
||||
comments: {
|
||||
cache: {
|
||||
keys: ["#token", "slug"]
|
||||
},
|
||||
params: {
|
||||
slug: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("comments.list", { article: article._id.toString() });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new comment to an article.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
* @param {Object} comment - Comment fields
|
||||
*
|
||||
* @returns {Object} Comment entity
|
||||
*/
|
||||
addComment: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" },
|
||||
comment: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("comments.create", { article: article._id.toString(), comment: ctx.params.comment });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
* @param {String} commentID - Comment ID
|
||||
* @param {Object} comment - Comment fields
|
||||
*
|
||||
* @returns {Object} Comment entity
|
||||
*/
|
||||
updateComment: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" },
|
||||
commentID: { type: "string" },
|
||||
comment: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found", 404));
|
||||
|
||||
return ctx.call("comments.update", { id: ctx.params.commentID, comment: ctx.params.comment });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} slug - Article slug
|
||||
* @param {String} commentID - Comment ID
|
||||
*
|
||||
* @returns {Number} Count of removed comment
|
||||
*/
|
||||
removeComment: {
|
||||
auth: "required",
|
||||
params: {
|
||||
slug: { type: "string" },
|
||||
commentID: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.Promise.resolve(ctx.params.slug)
|
||||
.then(slug => this.findBySlug(slug))
|
||||
.then(article => {
|
||||
if (!article)
|
||||
return this.Promise.reject(new MoleculerClientError("Article not found"));
|
||||
|
||||
return ctx.call("comments.remove", { id: ctx.params.commentID });
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Find an article by slug
|
||||
*
|
||||
* @param {String} slug - Article slug
|
||||
*
|
||||
* @results {Object} Promise<Article
|
||||
*/
|
||||
findBySlug(slug) {
|
||||
return this.adapter.findOne({ slug });
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform the result entities to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Array} entities
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformResult(ctx, entities, user) {
|
||||
if (Array.isArray(entities)) {
|
||||
return this.Promise.map(entities, item => this.transformEntity(ctx, item, user))
|
||||
.then(articles => ({ articles }));
|
||||
} else {
|
||||
return this.transformEntity(ctx, entities, user)
|
||||
.then(article => ({ article }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a result entity to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} entity
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformEntity(ctx, entity, user) {
|
||||
if (!entity) return this.Promise.resolve();
|
||||
|
||||
return this.Promise.resolve(entity);
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.articles"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.comments"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.favorites"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
266
intro/server/services/comments.service.js
Normal file
@@ -0,0 +1,266 @@
|
||||
"use strict";
|
||||
|
||||
const { ForbiddenError } = require("moleculer-web").Errors;
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "comments",
|
||||
mixins: [DbService("comments")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
fields: ["_id", "author", "article", "body", "createdAt", "updatedAt"],
|
||||
populates: {
|
||||
"author": {
|
||||
action: "users.get",
|
||||
params: {
|
||||
fields: ["_id", "username", "bio", "image"]
|
||||
}
|
||||
}
|
||||
},
|
||||
entityValidator: {
|
||||
article: { type: "string" },
|
||||
body: { type: "string", min: 1 },
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* Create a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} article - Article ID
|
||||
* @param {Object} comment - Comment entity
|
||||
*
|
||||
* @returns {Object} Created comment entity
|
||||
*/
|
||||
create: {
|
||||
auth: "required",
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
comment: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
let entity = ctx.params.comment;
|
||||
entity.article = ctx.params.article;
|
||||
entity.author = ctx.meta.user._id.toString();
|
||||
|
||||
return this.validateEntity(entity)
|
||||
.then(() => {
|
||||
|
||||
entity.createdAt = new Date();
|
||||
entity.updatedAt = new Date();
|
||||
|
||||
return this.adapter.insert(entity)
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update a comment.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Comment ID
|
||||
* @param {Object} comment - Comment modified fields
|
||||
*
|
||||
* @returns {Object} Updated comment entity
|
||||
*/
|
||||
update: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "string" },
|
||||
comment: { type: "object", props: {
|
||||
body: { type: "string", min: 1 },
|
||||
} }
|
||||
},
|
||||
handler(ctx) {
|
||||
let newData = ctx.params.comment;
|
||||
newData.updatedAt = new Date();
|
||||
|
||||
return this.getById(ctx.params.id)
|
||||
.then(comment => {
|
||||
if (comment.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
const update = {
|
||||
"$set": newData
|
||||
};
|
||||
|
||||
return this.adapter.updateById(ctx.params.id, update);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, { populate: ["author"]}, doc))
|
||||
.then(entity => this.transformResult(ctx, entity, ctx.meta.user))
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* List of comments by article.
|
||||
*
|
||||
* @actions
|
||||
* @param {String} article - Article ID
|
||||
* @param {Number} limit - Pagination limit
|
||||
* @param {Number} offset - Pagination offset
|
||||
*
|
||||
* @returns {Object} List of comments
|
||||
*/
|
||||
list: {
|
||||
cache: {
|
||||
keys: ["#token", "article", "limit", "offset"]
|
||||
},
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
limit: { type: "number", optional: true, convert: true },
|
||||
offset: { type: "number", optional: true, convert: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
const limit = ctx.params.limit ? Number(ctx.params.limit) : 20;
|
||||
const offset = ctx.params.offset ? Number(ctx.params.offset) : 0;
|
||||
|
||||
let params = {
|
||||
limit,
|
||||
offset,
|
||||
sort: ["-createdAt"],
|
||||
populate: ["author"],
|
||||
query: {
|
||||
article: ctx.params.article
|
||||
}
|
||||
};
|
||||
let countParams;
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
countParams = Object.assign({}, params);
|
||||
// Remove pagination params
|
||||
if (countParams && countParams.limit)
|
||||
countParams.limit = null;
|
||||
if (countParams && countParams.offset)
|
||||
countParams.offset = null;
|
||||
})
|
||||
.then(() => this.Promise.all([
|
||||
// Get rows
|
||||
this.adapter.find(params),
|
||||
|
||||
// Get count of all rows
|
||||
this.adapter.count(countParams)
|
||||
|
||||
])).then(res => {
|
||||
return this.transformDocuments(ctx, params, res[0])
|
||||
.then(docs => this.transformResult(ctx, docs, ctx.meta.user))
|
||||
.then(r => {
|
||||
r.commentsCount = res[1];
|
||||
return r;
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a comment
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
* @param {String} id - Comment ID
|
||||
*
|
||||
* @returns {Number} Count of removed comments
|
||||
*/
|
||||
remove: {
|
||||
auth: "required",
|
||||
params: {
|
||||
id: { type: "any" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.getById(ctx.params.id)
|
||||
.then(comment => {
|
||||
if (comment.author !== ctx.meta.user._id.toString())
|
||||
return this.Promise.reject(new ForbiddenError());
|
||||
|
||||
return this.adapter.removeById(ctx.params.id)
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
|
||||
/**
|
||||
* Transform the result entities to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Array} entities
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformResult(ctx, entities, user) {
|
||||
if (Array.isArray(entities)) {
|
||||
return this.Promise.map(entities, item => this.transformEntity(ctx, item, user))
|
||||
.then(comments => ({ comments }));
|
||||
} else {
|
||||
return this.transformEntity(ctx, entities, user)
|
||||
.then(comment => ({ comment }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform a result entity to follow the RealWorld API spec
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} entity
|
||||
* @param {Object} user - Logged in user
|
||||
*/
|
||||
transformEntity(ctx, entity, loggedInUser) {
|
||||
if (!entity) return this.Promise.resolve();
|
||||
|
||||
return this.Promise.resolve(entity)
|
||||
.then(entity => {
|
||||
entity.id = entity._id;
|
||||
|
||||
if (loggedInUser) {
|
||||
return ctx.call("follows.has", { user: loggedInUser._id.toString(), follow: entity.author._id })
|
||||
.then(res => {
|
||||
entity.author.following = res;
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
entity.author.following = false;
|
||||
|
||||
return entity;
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.comments"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.articles"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
177
intro/server/services/favorites.service.js
Normal file
@@ -0,0 +1,177 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
|
||||
module.exports = {
|
||||
name: "favorites",
|
||||
mixins: [DbService("favorites")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Create a new favorite record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
* @returns {Object} Created favorite record
|
||||
*/
|
||||
add: {
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
user: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { article, user } = ctx.params;
|
||||
return this.findByArticleAndUser(article, user)
|
||||
.then(item => {
|
||||
if (item)
|
||||
return this.Promise.reject(new MoleculerClientError("Articles has already favorited"));
|
||||
|
||||
return this.adapter.insert({ article, user, createdAt: new Date() })
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the given 'article' is followed by 'user'.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
has: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
user: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { article, user } = ctx.params;
|
||||
return this.findByArticleAndUser(article, user)
|
||||
.then(item => !!item);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Count of favorites.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String?} article - Article ID
|
||||
* @param {String?} user - User ID
|
||||
* @returns {Number}
|
||||
*/
|
||||
count: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
article: { type: "string", optional: true },
|
||||
user: { type: "string", optional: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
let query = {};
|
||||
if (ctx.params.article)
|
||||
query = { article: ctx.params.article };
|
||||
|
||||
if (ctx.params.user)
|
||||
query = { user: ctx.params.user };
|
||||
|
||||
return this.adapter.count({ query });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a favorite record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
* @returns {Number} Count of removed records
|
||||
*/
|
||||
delete: {
|
||||
params: {
|
||||
article: { type: "string" },
|
||||
user: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { article, user } = ctx.params;
|
||||
return this.findByArticleAndUser(article, user)
|
||||
.then(item => {
|
||||
if (!item)
|
||||
return this.Promise.reject(new MoleculerClientError("Articles has not favorited yet"));
|
||||
|
||||
return this.adapter.removeById(item._id)
|
||||
.then(json => this.entityChanged("removed", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove all favorites by article
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} article - Article ID
|
||||
* @returns {Number} Count of removed records
|
||||
*/
|
||||
removeByArticle: {
|
||||
params: {
|
||||
article: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.removeMany(ctx.params);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Find the first favorite record by 'article' or 'user'
|
||||
* @param {String} article - Article ID
|
||||
* @param {String} user - User ID
|
||||
*/
|
||||
findByArticleAndUser(article, user) {
|
||||
return this.adapter.findOne({ article, user });
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.favorites"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.articles"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
153
intro/server/services/follows.service.js
Normal file
@@ -0,0 +1,153 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "follows",
|
||||
mixins: [DbService("follows")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
|
||||
/**
|
||||
* Create a new following record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} user - Follower username
|
||||
* @param {String} follow - Followee username
|
||||
* @returns {Object} Created following record
|
||||
*/
|
||||
add: {
|
||||
params: {
|
||||
user: { type: "string" },
|
||||
follow: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { follow, user } = ctx.params;
|
||||
return this.findByFollowAndUser(follow, user)
|
||||
.then(item => {
|
||||
if (item)
|
||||
return this.Promise.reject(new MoleculerClientError("User has already followed"));
|
||||
|
||||
return this.adapter.insert({ follow, user, createdAt: new Date() })
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the given 'follow' user is followed by 'user' user.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} user - Follower username
|
||||
* @param {String} follow - Followee username
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
has: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
user: { type: "string" },
|
||||
follow: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.findByFollowAndUser(ctx.params.follow, ctx.params.user)
|
||||
.then(item => !!item);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Count of following.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String?} user - Follower username
|
||||
* @param {String?} follow - Followee username
|
||||
* @returns {Number}
|
||||
*/
|
||||
count: {
|
||||
cache: {
|
||||
keys: ["article", "user"]
|
||||
},
|
||||
params: {
|
||||
follow: { type: "string", optional: true },
|
||||
user: { type: "string", optional: true },
|
||||
},
|
||||
handler(ctx) {
|
||||
let query = {};
|
||||
if (ctx.params.follow)
|
||||
query = { follow: ctx.params.follow };
|
||||
|
||||
if (ctx.params.user)
|
||||
query = { user: ctx.params.user };
|
||||
|
||||
return this.adapter.count({ query });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a following record
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} user - Follower username
|
||||
* @param {String} follow - Followee username
|
||||
* @returns {Number} Count of removed records
|
||||
*/
|
||||
delete: {
|
||||
params: {
|
||||
user: { type: "string" },
|
||||
follow: { type: "string" },
|
||||
},
|
||||
handler(ctx) {
|
||||
const { follow, user } = ctx.params;
|
||||
return this.findByFollowAndUser(follow, user)
|
||||
.then(item => {
|
||||
if (!item)
|
||||
return this.Promise.reject(new MoleculerClientError("User has not followed yet"));
|
||||
|
||||
return this.adapter.removeById(item._id)
|
||||
.then(json => this.entityChanged("removed", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Find the first following record by 'follow' or 'user'
|
||||
* @param {String} follow - Followee username
|
||||
* @param {String} user - Follower username
|
||||
*/
|
||||
findByFollowAndUser(follow, user) {
|
||||
return this.adapter.findOne({ follow, user });
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
96
intro/server/services/metrics.service.js
Normal file
@@ -0,0 +1,96 @@
|
||||
let _ = require("lodash");
|
||||
|
||||
module.exports = {
|
||||
name: "metrics",
|
||||
|
||||
events: {
|
||||
"metrics.trace.span.start"(payload) {
|
||||
this.requests[payload.id] = payload;
|
||||
payload.spans = [];
|
||||
|
||||
if (payload.parent) {
|
||||
let parent = this.requests[payload.parent];
|
||||
if (parent)
|
||||
parent.spans.push(payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
"metrics.trace.span.finish"(payload) {
|
||||
let item = this.requests[payload.id];
|
||||
_.assign(item, payload);
|
||||
|
||||
if (!payload.parent)
|
||||
this.printRequest(payload.id);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
printRequest(id) {
|
||||
let main = this.requests[id];
|
||||
|
||||
let w = 73;
|
||||
let r = _.repeat;
|
||||
let gw = 35;
|
||||
let maxTitle = w - 2 - 2 - gw - 2 - 1;
|
||||
|
||||
this.logger.info(["┌", r("─", w-2), "┐"].join(""));
|
||||
|
||||
let printSpanTime = (span) => {
|
||||
let time = span.duration.toFixed(2);
|
||||
|
||||
let maxActionName = maxTitle - (span.level-1) * 2 - time.length - 3 - (span.fromCache ? 2 : 0) - (span.remoteCall ? 2 : 0) - (span.error ? 2 : 0);
|
||||
let actionName = span.action ? span.action.name : "";
|
||||
if (actionName.length > maxActionName)
|
||||
actionName = _.truncate(span.action.name, { length: maxActionName });
|
||||
|
||||
let strAction = [
|
||||
r(" ", span.level - 1),
|
||||
actionName,
|
||||
r(" ", maxActionName - actionName.length + 1),
|
||||
span.fromCache ? "* " : "",
|
||||
span.remoteCall ? "» " : "",
|
||||
span.error ? "× " : "",
|
||||
time,
|
||||
"ms "
|
||||
].join("");
|
||||
|
||||
if (span.startTime == null || span.endTime == null) {
|
||||
this.logger.info(strAction + "! Missing invoke !");
|
||||
return;
|
||||
}
|
||||
|
||||
let gstart = (span.startTime - main.startTime) / (main.endTime - main.startTime) * 100;
|
||||
let gstop = (span.endTime - main.startTime) / (main.endTime - main.startTime) * 100;
|
||||
|
||||
if (_.isNaN(gstart) && _.isNaN(gstop)) {
|
||||
gstart = 0;
|
||||
gstop = 100;
|
||||
}
|
||||
|
||||
let p1 = Math.round(gw * gstart / 100);
|
||||
let p2 = Math.round(gw * gstop / 100) - p1;
|
||||
let p3 = Math.max(gw - (p1 + p2), 0);
|
||||
|
||||
let gauge = [
|
||||
"[",
|
||||
r(".", p1),
|
||||
r("■", p2),
|
||||
r(".", p3),
|
||||
"]"
|
||||
].join("");
|
||||
|
||||
this.logger.info("│ " + strAction + gauge + " │");
|
||||
|
||||
if (span.spans.length > 0)
|
||||
span.spans.forEach(spanID => printSpanTime(this.requests[spanID]));
|
||||
};
|
||||
|
||||
printSpanTime(main);
|
||||
this.logger.info(["└", r("─", w-2), "┘"].join(""));
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.requests = {};
|
||||
}
|
||||
};
|
||||
390
intro/server/services/users.service.js
Normal file
@@ -0,0 +1,390 @@
|
||||
"use strict";
|
||||
|
||||
const { MoleculerClientError } = require("moleculer").Errors;
|
||||
|
||||
//const crypto = require("crypto");
|
||||
const bcrypt = require("bcrypt-nodejs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const DbService = require("../mixins/db.mixin");
|
||||
|
||||
module.exports = {
|
||||
name: "users",
|
||||
mixins: [DbService("users")],
|
||||
|
||||
/**
|
||||
* Default settings
|
||||
*/
|
||||
settings: {
|
||||
/** Secret for JWT */
|
||||
JWT_SECRET: process.env.JWT_SECRET || "jwt-conduit-secret",
|
||||
|
||||
/** Public fields */
|
||||
fields: ["_id", "username", "email", "bio", "image"],
|
||||
|
||||
/** Validator schema for entity */
|
||||
entityValidator: {
|
||||
username: { type: "string", min: 2, pattern: /^[a-zA-Z0-9]+$/ },
|
||||
password: { type: "string", min: 6 },
|
||||
email: { type: "email" },
|
||||
bio: { type: "string", optional: true },
|
||||
image: { type: "string", optional: true },
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
actions: {
|
||||
/**
|
||||
* Register a new user
|
||||
*
|
||||
* @actions
|
||||
* @param {Object} user - User entity
|
||||
*
|
||||
* @returns {Object} Created entity & token
|
||||
*/
|
||||
create: {
|
||||
params: {
|
||||
user: { type: "object" }
|
||||
},
|
||||
handler(ctx) {
|
||||
let entity = ctx.params.user;
|
||||
return this.validateEntity(entity)
|
||||
.then(() => {
|
||||
if (entity.username)
|
||||
return this.adapter.findOne({ username: entity.username })
|
||||
.then(found => {
|
||||
if (found)
|
||||
return Promise.reject(new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist"}]));
|
||||
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (entity.email)
|
||||
return this.adapter.findOne({ email: entity.email })
|
||||
.then(found => {
|
||||
if (found)
|
||||
return Promise.reject(new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist"}]));
|
||||
});
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
entity.password = bcrypt.hashSync(entity.password, bcrypt.genSaltSync(10));
|
||||
entity.bio = entity.bio || "";
|
||||
entity.image = entity.image || null;
|
||||
entity.createdAt = new Date();
|
||||
|
||||
return this.adapter.insert(entity)
|
||||
.then(doc => this.transformDocuments(ctx, {}, doc))
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token))
|
||||
.then(json => this.entityChanged("created", json, ctx).then(() => json));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Login with username & password
|
||||
*
|
||||
* @actions
|
||||
* @param {Object} user - User credentials
|
||||
*
|
||||
* @returns {Object} Logged in user with token
|
||||
*/
|
||||
login: {
|
||||
params: {
|
||||
user: { type: "object", props: {
|
||||
email: { type: "email" },
|
||||
password: { type: "string", min: 1 }
|
||||
}}
|
||||
},
|
||||
handler(ctx) {
|
||||
const { email, password } = ctx.params.user;
|
||||
|
||||
return this.Promise.resolve()
|
||||
.then(() => this.adapter.findOne({ email }))
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("Email or password is invalid!", 422, "", [{ field: "email", message: "is not found"}]));
|
||||
|
||||
return bcrypt.compare(password, user.password).then(res => {
|
||||
if (!res)
|
||||
return Promise.reject(new MoleculerClientError("Wrong password!", 422, "", [{ field: "email", message: "is not found"}]));
|
||||
|
||||
// Transform user entity (remove password and all protected fields)
|
||||
return this.transformDocuments(ctx, {}, user);
|
||||
});
|
||||
})
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get user by JWT token (for API GW authentication)
|
||||
*
|
||||
* @actions
|
||||
* @param {String} token - JWT token
|
||||
*
|
||||
* @returns {Object} Resolved user
|
||||
*/
|
||||
resolveToken: {
|
||||
cache: {
|
||||
keys: ["token"],
|
||||
ttl: 60 * 60 // 1 hour
|
||||
},
|
||||
params: {
|
||||
token: "string"
|
||||
},
|
||||
handler(ctx) {
|
||||
return new this.Promise((resolve, reject) => {
|
||||
jwt.verify(ctx.params.token, this.settings.JWT_SECRET, (err, decoded) => {
|
||||
if (err)
|
||||
return reject(err);
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
|
||||
})
|
||||
.then(decoded => {
|
||||
if (decoded.id)
|
||||
return this.getById(decoded.id);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user entity.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @returns {Object} User entity
|
||||
*/
|
||||
me: {
|
||||
auth: "required",
|
||||
cache: {
|
||||
keys: ["#token"]
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.getById(ctx.meta.user._id)
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 400));
|
||||
|
||||
return this.transformDocuments(ctx, {}, user);
|
||||
})
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update current user entity.
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {Object} user - Modified fields
|
||||
* @returns {Object} User entity
|
||||
*/
|
||||
updateMyself: {
|
||||
auth: "required",
|
||||
params: {
|
||||
user: { type: "object", props: {
|
||||
username: { type: "string", min: 2, optional: true, pattern: /^[a-zA-Z0-9]+$/ },
|
||||
password: { type: "string", min: 6, optional: true },
|
||||
email: { type: "email", optional: true },
|
||||
bio: { type: "string", optional: true },
|
||||
image: { type: "string", optional: true },
|
||||
}}
|
||||
},
|
||||
handler(ctx) {
|
||||
const newData = ctx.params.user;
|
||||
return this.Promise.resolve()
|
||||
.then(() => {
|
||||
if (newData.username)
|
||||
return this.adapter.findOne({ username: newData.username })
|
||||
.then(found => {
|
||||
if (found && found._id.toString() !== ctx.meta.user._id.toString())
|
||||
return Promise.reject(new MoleculerClientError("Username is exist!", 422, "", [{ field: "username", message: "is exist"}]));
|
||||
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
if (newData.email)
|
||||
return this.adapter.findOne({ email: newData.email })
|
||||
.then(found => {
|
||||
if (found && found._id.toString() !== ctx.meta.user._id.toString())
|
||||
return Promise.reject(new MoleculerClientError("Email is exist!", 422, "", [{ field: "email", message: "is exist"}]));
|
||||
});
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
newData.updatedAt = new Date();
|
||||
const update = {
|
||||
"$set": newData
|
||||
};
|
||||
return this.adapter.updateById(ctx.meta.user._id, update);
|
||||
})
|
||||
.then(doc => this.transformDocuments(ctx, {}, doc))
|
||||
.then(user => this.transformEntity(user, true, ctx.meta.token))
|
||||
.then(json => this.entityChanged("updated", json, ctx).then(() => json));
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a user profile.
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} username - Username
|
||||
* @returns {Object} User entity
|
||||
*/
|
||||
profile: {
|
||||
cache: {
|
||||
keys: ["#token", "username"]
|
||||
},
|
||||
params: {
|
||||
username: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.findOne({ username: ctx.params.username })
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
|
||||
|
||||
return this.transformDocuments(ctx, {}, user);
|
||||
})
|
||||
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Follow a user
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} username - Followed username
|
||||
* @returns {Object} Current user entity
|
||||
*/
|
||||
follow: {
|
||||
auth: "required",
|
||||
params: {
|
||||
username: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.findOne({ username: ctx.params.username })
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
|
||||
|
||||
return ctx.call("follows.add", { user: ctx.meta.user._id.toString(), follow: user._id.toString() })
|
||||
.then(() => this.transformDocuments(ctx, {}, user));
|
||||
})
|
||||
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unfollow a user
|
||||
* Auth is required!
|
||||
*
|
||||
* @actions
|
||||
*
|
||||
* @param {String} username - Unfollowed username
|
||||
* @returns {Object} Current user entity
|
||||
*/
|
||||
unfollow: {
|
||||
auth: "required",
|
||||
params: {
|
||||
username: { type: "string" }
|
||||
},
|
||||
handler(ctx) {
|
||||
return this.adapter.findOne({ username: ctx.params.username })
|
||||
.then(user => {
|
||||
if (!user)
|
||||
return this.Promise.reject(new MoleculerClientError("User not found!", 404));
|
||||
|
||||
return ctx.call("follows.delete", { user: ctx.meta.user._id.toString(), follow: user._id.toString() })
|
||||
.then(() => this.transformDocuments(ctx, {}, user));
|
||||
})
|
||||
.then(user => this.transformProfile(ctx, user, ctx.meta.user));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
methods: {
|
||||
/**
|
||||
* Generate a JWT token from user entity
|
||||
*
|
||||
* @param {Object} user
|
||||
*/
|
||||
generateJWT(user) {
|
||||
const today = new Date();
|
||||
const exp = new Date(today);
|
||||
exp.setDate(today.getDate() + 60);
|
||||
|
||||
return jwt.sign({
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
exp: Math.floor(exp.getTime() / 1000)
|
||||
}, this.settings.JWT_SECRET);
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform returned user entity. Generate JWT token if neccessary.
|
||||
*
|
||||
* @param {Object} user
|
||||
* @param {Boolean} withToken
|
||||
*/
|
||||
transformEntity(user, withToken, token) {
|
||||
if (user) {
|
||||
//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
|
||||
user.image = user.image || "";
|
||||
if (withToken)
|
||||
user.token = token || this.generateJWT(user);
|
||||
}
|
||||
|
||||
return { user };
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform returned user entity as profile.
|
||||
*
|
||||
* @param {Context} ctx
|
||||
* @param {Object} user
|
||||
* @param {Object?} loggedInUser
|
||||
*/
|
||||
transformProfile(ctx, user, loggedInUser) {
|
||||
//user.image = user.image || "https://www.gravatar.com/avatar/" + crypto.createHash("md5").update(user.email).digest("hex") + "?d=robohash";
|
||||
user.image = user.image || "/assets/images/smiley-cyrus.jpg";
|
||||
|
||||
if (loggedInUser) {
|
||||
return ctx.call("follows.has", { user: loggedInUser._id.toString(), follow: user._id.toString() })
|
||||
.then(res => {
|
||||
user.following = res;
|
||||
return { profile: user };
|
||||
});
|
||||
}
|
||||
|
||||
user.following = false;
|
||||
|
||||
return { profile: user };
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
"cache.clean.users"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
},
|
||||
"cache.clean.follows"() {
|
||||
if (this.broker.cacher)
|
||||
this.broker.cacher.clean(`${this.name}.*`);
|
||||
}
|
||||
}
|
||||
};
|
||||
62
intro/server/src/Main.elm
Normal file
@@ -0,0 +1,62 @@
|
||||
port module Main exposing (..)
|
||||
|
||||
{- | Displays “You’re all set!” and a heart in the style of the Elm logo (created
|
||||
by Marco Perone, CC-BY-SA-4.0 - thanks for sharing it, Marco!)
|
||||
|
||||
If this doesn't display, it means something needs to be fixed about your local
|
||||
setup, and you should ask the instructor for help!
|
||||
-}
|
||||
|
||||
import Browser
|
||||
import Html exposing (Html, h1, img, section, text)
|
||||
import Html.Attributes exposing (alt, src, style)
|
||||
import Json.Decode exposing (Value)
|
||||
|
||||
|
||||
port storeSession : Maybe String -> Cmd msg
|
||||
|
||||
|
||||
port onSessionChange : (Value -> msg) -> Sub msg
|
||||
|
||||
|
||||
main : Program () () ()
|
||||
main =
|
||||
Browser.application
|
||||
{ init = \_ _ _ -> ( (), Cmd.none )
|
||||
, onUrlChange = \_ -> ()
|
||||
, onUrlRequest = \_ -> ()
|
||||
, update =
|
||||
\() () ->
|
||||
if False then
|
||||
( (), storeSession Nothing )
|
||||
|
||||
else
|
||||
( (), Cmd.none )
|
||||
, subscriptions = \() -> onSessionChange (\_ -> ())
|
||||
, view =
|
||||
\() ->
|
||||
{ title = "Elm 0.19 workshop"
|
||||
, body =
|
||||
[ section
|
||||
[ style "margin" "40px auto"
|
||||
, style "width" "960px"
|
||||
, style "text-align" "center"
|
||||
]
|
||||
[ h1
|
||||
[ style "margin" "40px auto"
|
||||
, style "font-family" "Helvetica, Arial, sans-serif"
|
||||
, style "font-size" "128px"
|
||||
, style "color" "rgb(90, 99, 120)"
|
||||
]
|
||||
[ text "You’re all set!" ]
|
||||
, img
|
||||
[ alt "Heart in the style of the Elm logo, by Marco Perone"
|
||||
, src "https://user-images.githubusercontent.com/1094080/39399444-a90f2746-4aeb-11e8-9bd6-4fe45e535921.png"
|
||||
, style "width" "368px"
|
||||
, style "height" "305px"
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||