first commit

This commit is contained in:
Nicholai Nissen 2021-04-01 19:58:41 +02:00
commit 8ee077cb7f
50 changed files with 33277 additions and 0 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

72
.gitignore vendored Normal file
View File

@ -0,0 +1,72 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# JetBrains IDE
.idea
# Don't track transpiled files
dist/
#
.adminbro
.vscode
data/

685
common/package-lock.json generated Normal file
View File

@ -0,0 +1,685 @@
{
"name": "common",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"typescript": "4.2"
},
"devDependencies": {
"mongoose": "^5.11.18"
}
},
"node_modules/@types/bson": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
"integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/mongodb": {
"version": "3.6.8",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.8.tgz",
"integrity": "sha512-8qNbL5/GFrljXc/QijcuQcUMYZ1iWNcqnJ6tneROwbfU0LsAjQ9bmq3aHi5lWXM4cyBPd2F/n9INAk/pZZttHw==",
"dev": true,
"dependencies": {
"@types/bson": "*",
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "14.14.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==",
"dev": true
},
"node_modules/bl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"dev": true,
"dependencies": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
}
},
"node_modules/bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
"dev": true
},
"node_modules/bson": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
"integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==",
"dev": true,
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
"node_modules/debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/debug/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"node_modules/denque": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==",
"dev": true,
"engines": {
"node": ">=0.10"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"node_modules/kareem": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==",
"dev": true
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"dev": true,
"optional": true
},
"node_modules/mongodb": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
"integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
"dev": true,
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"require_optional": "^1.0.1",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
},
"engines": {
"node": ">=4"
},
"optionalDependencies": {
"saslprep": "^1.0.0"
},
"peerDependenciesMeta": {
"aws4": {
"optional": true
},
"bson-ext": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"mongodb-extjson": {
"optional": true
},
"snappy": {
"optional": true
}
}
},
"node_modules/mongoose": {
"version": "5.11.18",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.18.tgz",
"integrity": "sha512-RsrPR9nhkXZbO3ml0DcmdbfeMvFNhgFrP81S6o1P+lFnDTNEKYnGNRCIL+ojD69wj7H5jJaAdZ0SJ5IlKxCHqw==",
"dev": true,
"dependencies": {
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
"kareem": "2.3.2",
"mongodb": "3.6.4",
"mongoose-legacy-pluralize": "1.0.2",
"mpath": "0.8.3",
"mquery": "3.2.4",
"ms": "2.1.2",
"regexp-clone": "1.0.0",
"safe-buffer": "5.2.1",
"sift": "7.0.1",
"sliced": "1.0.1"
},
"engines": {
"node": ">=4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose-legacy-pluralize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
"integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==",
"dev": true,
"peerDependencies": {
"mongoose": "*"
}
},
"node_modules/mpath": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz",
"integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==",
"dev": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mquery": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.4.tgz",
"integrity": "sha512-uOLpp7iRX0BV1Uu6YpsqJ5b42LwYnmu0WeF/f8qgD/On3g0XDaQM6pfn0m6UxO6SM8DioZ9Bk6xxbWIGHm2zHg==",
"dev": true,
"dependencies": {
"bluebird": "3.5.1",
"debug": "3.1.0",
"regexp-clone": "^1.0.0",
"safe-buffer": "5.1.2",
"sliced": "1.0.1"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mquery/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"node_modules/regexp-clone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==",
"dev": true
},
"node_modules/require_optional": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
"dev": true,
"dependencies": {
"resolve-from": "^2.0.0",
"semver": "^5.1.0"
}
},
"node_modules/resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dev": true,
"optional": true,
"dependencies": {
"sparse-bitfield": "^3.0.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/sift": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz",
"integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==",
"dev": true
},
"node_modules/sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=",
"dev": true
},
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
"dev": true,
"optional": true,
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"node_modules/typescript": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz",
"integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
}
},
"dependencies": {
"@types/bson": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
"integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/mongodb": {
"version": "3.6.8",
"resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.8.tgz",
"integrity": "sha512-8qNbL5/GFrljXc/QijcuQcUMYZ1iWNcqnJ6tneROwbfU0LsAjQ9bmq3aHi5lWXM4cyBPd2F/n9INAk/pZZttHw==",
"dev": true,
"requires": {
"@types/bson": "*",
"@types/node": "*"
}
},
"@types/node": {
"version": "14.14.31",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz",
"integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==",
"dev": true
},
"bl": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"dev": true,
"requires": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
}
},
"bluebird": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
"integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==",
"dev": true
},
"bson": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
"integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==",
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"dev": true
},
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
},
"dependencies": {
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
}
}
},
"denque": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==",
"dev": true
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"kareem": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz",
"integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==",
"dev": true
},
"memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"dev": true,
"optional": true
},
"mongodb": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
"integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
"dev": true,
"requires": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"require_optional": "^1.0.1",
"safe-buffer": "^5.1.2",
"saslprep": "^1.0.0"
}
},
"mongoose": {
"version": "5.11.18",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.11.18.tgz",
"integrity": "sha512-RsrPR9nhkXZbO3ml0DcmdbfeMvFNhgFrP81S6o1P+lFnDTNEKYnGNRCIL+ojD69wj7H5jJaAdZ0SJ5IlKxCHqw==",
"dev": true,
"requires": {
"@types/mongodb": "^3.5.27",
"bson": "^1.1.4",
"kareem": "2.3.2",
"mongodb": "3.6.4",
"mongoose-legacy-pluralize": "1.0.2",
"mpath": "0.8.3",
"mquery": "3.2.4",
"ms": "2.1.2",
"regexp-clone": "1.0.0",
"safe-buffer": "5.2.1",
"sift": "7.0.1",
"sliced": "1.0.1"
}
},
"mongoose-legacy-pluralize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz",
"integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==",
"dev": true,
"requires": {}
},
"mpath": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz",
"integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==",
"dev": true
},
"mquery": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.4.tgz",
"integrity": "sha512-uOLpp7iRX0BV1Uu6YpsqJ5b42LwYnmu0WeF/f8qgD/On3g0XDaQM6pfn0m6UxO6SM8DioZ9Bk6xxbWIGHm2zHg==",
"dev": true,
"requires": {
"bluebird": "3.5.1",
"debug": "3.1.0",
"regexp-clone": "^1.0.0",
"safe-buffer": "5.1.2",
"sliced": "1.0.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
}
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
}
}
},
"regexp-clone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
"integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==",
"dev": true
},
"require_optional": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
"dev": true,
"requires": {
"resolve-from": "^2.0.0",
"semver": "^5.1.0"
}
},
"resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
"dev": true
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"saslprep": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dev": true,
"optional": true,
"requires": {
"sparse-bitfield": "^3.0.3"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"sift": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz",
"integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==",
"dev": true
},
"sliced": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz",
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=",
"dev": true
},
"sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
"dev": true,
"optional": true,
"requires": {
"memory-pager": "^1.0.2"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
}
}
},
"typescript": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz",
"integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
}
}
}

17
common/package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "common",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "4.2"
},
"devDependencies": {
"mongoose": "^5.11.18"
}
}

18
common/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"pretty": true,
"target": "es2019",
"outDir": "dist"
},
"include": ["./**/*"],
"exclude": [
"node_modules/**/*",
"test"
]
}

View File

@ -0,0 +1,12 @@
export interface LiteratureItem {
publicationYear: number
publisher: string
name: string
authors: string[]
}
export interface LiteratureList {
name: string
description: string
literature: LiteratureItem[]
}

View File

@ -0,0 +1,4 @@
export interface RestResponse<T> {
status: number
}

View File

@ -0,0 +1,15 @@
{
"folders": [
{
"name": "Root",
"path": "."
}, {
"name": "Services",
"path": "services"
}, {
"name": "Common",
"path": "common"
}
],
"settings": {}
}

6
services/.dockerignore Normal file
View File

@ -0,0 +1,6 @@
docker-compose.env
docker-compose.yml
Dockerfile
node_modules
test
.vscode

35
services/.editorconfig Normal file
View File

@ -0,0 +1,35 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
space_after_anon_function = true
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 4
[{package,bower}.json]
indent_style = space
indent_size = 2
[*.{yml,yaml}]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
[*.{js,ts}]
quote_type = "single"

143
services/.eslintrc.js Normal file
View File

@ -0,0 +1,143 @@
module.exports = {
env: {
browser: true,
es6: true,
node: true
},
ignorePatterns: [ "test/*"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
sourceType: "module"
},
plugins: ["prefer-arrow", "import", "@typescript-eslint"],
rules: {
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/class-name-casing": "off",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-definitions": "error",
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
accessibility: "explicit"
}
],
"@typescript-eslint/indent": [
"off",
4,
{
FunctionDeclaration: {
parameters: "first"
},
FunctionExpression: {
parameters: "first"
}
}
],
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/member-delimiter-style": [
"error",
{
multiline: {
delimiter: "semi",
requireLast: true
},
singleline: {
delimiter: "semi",
requireLast: false
}
}
],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/no-empty-function": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/quotes": [
"error",
"double",
{
avoidEscape: true
}
],
"@typescript-eslint/semi": ["error", "always"],
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"arrow-body-style": "error",
"arrow-parens": ["error", "as-needed"],
camelcase: "error",
"capitalized-comments": "error",
"comma-dangle": ["error", "always-multiline"],
complexity: "off",
"constructor-super": "error",
curly: "error",
"dot-notation": "error",
"eol-last": "error",
eqeqeq: ["error", "smart"],
"guard-for-in": "error",
"id-blacklist": ["error", "any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"],
"id-match": "error",
"import/order": "error",
"max-classes-per-file": ["error", 1],
"max-len": [
"error",
{
"ignoreUrls": true ,
code: 160
}
],
"new-parens": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-cond-assign": "error",
"no-console": "off",
"no-debugger": "error",
"no-empty": "error",
"no-eval": "error",
"no-fallthrough": "off",
"no-invalid-this": "off",
"no-multiple-empty-lines": "error",
"no-new-wrappers": "error",
"no-shadow": [
"error",
{
hoist: "all"
}
],
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef-init": "error",
"no-underscore-dangle": "error",
"no-unsafe-finally": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-var": "error",
"object-shorthand": "error",
"one-var": ["error", "never"],
"prefer-arrow/prefer-arrow-functions": "error",
"prefer-const": "error",
"quote-props": ["error", "consistent-as-needed"],
radix: "error",
"space-before-function-paren": [
"error",
{
anonymous: "never",
asyncArrow: "always",
named: "never"
}
],
"spaced-comment": "error",
"use-isnan": "error",
"valid-typeof": "off",
}
};

67
services/.gitignore vendored Normal file
View File

@ -0,0 +1,67 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# JetBrains IDE
.idea
# Don't track transpiled files
dist/

19
services/Dockerfile Normal file
View File

@ -0,0 +1,19 @@
FROM node:lts-alpine
# Working directory
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci --silent
# Copy source
COPY . .
# Build and cleanup
ENV NODE_ENV=production
RUN npm run build \
&& npm prune
# Start server
CMD ["node", "./node_modules/moleculer/bin/moleculer-runner.js"]

42
services/README.md Normal file
View File

@ -0,0 +1,42 @@
[![Moleculer](https://badgen.net/badge/Powered%20by/Moleculer/0e83cd)](https://moleculer.services)
# redplateaus
This is a [Moleculer](https://moleculer.services/)-based microservices project. Generated with the [Moleculer CLI](https://moleculer.services/docs/0.14/moleculer-cli.html).
## Usage
Start the project with `npm run dev` command.
After starting, open the http://localhost:3000/ URL in your browser.
On the welcome page you can test the generated services via API Gateway and check the nodes & services.
In the terminal, try the following commands:
- `nodes` - List all connected nodes.
- `actions` - List all registered service actions.
- `call greeter.hello` - Call the `greeter.hello` action.
- `call greeter.welcome --name John` - Call the `greeter.welcome` action with the `name` parameter.
- `call products.list` - List the products (call the `products.list` action).
## Services
- **api**: API Gateway services
- **greeter**: Sample service with `hello` and `welcome` actions.
- **products**: Sample DB service. To use with MongoDB, set `MONGO_URI` environment variables and install MongoDB adapter with `npm i moleculer-db-adapter-mongo`.
## Mixins
- **db.mixin**: Database access mixin for services. Based on [moleculer-db](https://github.com/moleculerjs/moleculer-db#readme)
## Useful links
* Moleculer website: https://moleculer.services/
* Moleculer Documentation: https://moleculer.services/docs/0.14/
## NPM scripts
- `npm run dev`: Start development mode (load all services locally with hot-reload & REPL)
- `npm run start`: Start production mode (set `SERVICES` env variable to load certain services)
- `npm run cli`: Start a CLI and connect to production. Don't forget to set production namespace with `--ns` argument in script
- `npm run lint`: Run ESLint
- `npm run ci`: Run continuous test mode with watching
- `npm test`: Run tests & generate coverage report
- `npm run dc:up`: Start the stack with Docker Compose
- `npm run dc:down`: Stop the stack with Docker Compose

View File

@ -0,0 +1,9 @@
NAMESPACE=
LOGGER=true
LOGLEVEL=info
SERVICEDIR=dist/services
CONFIG=dist
CACHER=Memory
MONGO_URI=mongodb://mongo/redplateaus

View File

@ -0,0 +1,68 @@
version: "3.3"
services:
api:
build:
context: .
image: redplateaus
env_file: docker-compose.env
environment:
SERVICES: api
PORT: 3000
labels:
- "traefik.enable=true"
- "traefik.http.routers.api-gw.rule=PathPrefix(`/`)"
- "traefik.http.services.api-gw.loadbalancer.server.port=3000"
networks:
- internal
greeter:
build:
context: .
image: redplateaus
env_file: docker-compose.env
environment:
SERVICES: greeter
networks:
- internal
products:
build:
context: .
image: redplateaus
env_file: docker-compose.env
environment:
SERVICES: products
depends_on:
- mongo
networks:
- internal
mongo:
image: mongo:4
volumes:
- data:/data/db
networks:
- internal
traefik:
image: traefik:v2.1
command:
- "--api.insecure=true" # Don't do that in production!
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
ports:
- 3000:80
- 3001:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- internal
- default
networks:
internal:
volumes:
data:

211
services/k8s.yaml Normal file
View File

@ -0,0 +1,211 @@
#########################################################
# Common Environment variables ConfigMap
#########################################################
apiVersion: v1
kind: ConfigMap
metadata:
name: common-env
data:
NAMESPACE: ""
LOGLEVEL: info
SERVICEDIR: dist/services
CACHER: Memory
MONGO_URI: mongodb://mongo/redplateaus
---
#########################################################
# Service for Moleculer API Gateway service
#########################################################
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 3000
targetPort: 3000
---
#########################################################
# Ingress for Moleculer API Gateway
#########################################################
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: ingress
spec:
rules:
- host: redplateaus.127.0.0.1.nip.io
http:
paths:
- path: /
backend:
serviceName: api
servicePort: 3000
---
#########################################################
# API Gateway service
#########################################################
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
selector:
matchLabels:
app: api
replicas: 2
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: redplateaus
envFrom:
- configMapRef:
name: common-env
env:
- name: SERVICES
value: api
- name: PORT
value: "3000"
ports:
- containerPort: 3000
---
#########################################################
# Greeter service
#########################################################
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeter
spec:
selector:
matchLabels:
app: greeter
replicas: 2
template:
metadata:
labels:
app: greeter
spec:
containers:
- name: greeter
image: redplateaus
envFrom:
- configMapRef:
name: common-env
env:
- name: SERVICES
value: greeter
---
#########################################################
# Products service
#########################################################
apiVersion: apps/v1
kind: Deployment
metadata:
name: products
spec:
selector:
matchLabels:
app: products
replicas: 2
template:
metadata:
labels:
app: products
spec:
containers:
- name: products
image: redplateaus
envFrom:
- configMapRef:
name: common-env
env:
- name: SERVICES
value: products
---
#########################################################
# MongoDB server
#########################################################
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongo
labels:
app: mongo
spec:
selector:
matchLabels:
app: mongo
replicas: 1
serviceName: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- image: mongo
name: mongo
ports:
- containerPort: 27017
resources: {}
volumeMounts:
- mountPath: /data/db
name: mongo-data
volumes:
- name: mongo-data
persistentVolumeClaim:
claimName: mongo-data
---
#########################################################
# Persistent volume for MongoDB
#########################################################
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongo-data
labels:
name: mongo-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
---
#########################################################
# MongoDB service
#########################################################
apiVersion: v1
kind: Service
metadata:
name: mongo
labels:
app: mongo
spec:
ports:
- port: 27017
targetPort: 27017
selector:
app: mongo

View File

@ -0,0 +1,97 @@
'use strict'
import { existsSync } from 'fs'
import { sync } from 'mkdirp'
import { Context, Service, ServiceSchema } from 'moleculer'
import DbService from 'moleculer-db'
import MongooseAdapter from 'moleculer-db-adapter-mongoose'
import { Model, Document } from 'mongoose'
import { isNil } from 'ramda'
import { mongooseOptions } from '../util/mongoose.options'
export default class Connection implements Partial<ServiceSchema>, ThisType<Service> {
private cacheCleanEventName: string
private collection: string
private schema: Partial<ServiceSchema> & ThisType<Service>
public constructor(private model: Model<Document>) {
this.collection = model.modelName
this.cacheCleanEventName = `cache.clean.${this.collection}`
this.schema = {
mixins: [DbService],
model: this.model,
events: {
/**
* Subscribe to the cache clean event. If it's triggered
* clean the cache entries for this service.
*/
async [this.cacheCleanEventName](this: DbService.DbService) {
if (this.broker.cacher) {
await this.broker.cacher.clean(`${this.fullName}.*`)
}
}
},
methods: {
/**
* Send a cache clearing event when an entity changed.
* TODO: Granularise?
* @param {String} type
* @param {any} json
* @param {Context} ctx
*/
entityChanged: async (type: string, json: any, ctx: Context) => {
await ctx.broadcast(this.cacheCleanEventName)
},
},
async started() {
/*
* Check the count of items in the DB. If it's empty,
* Call the `seedDB` method of the service.
*/
if (this.seedDB) {
const count = await this.adapter.count()
if (count === 0) {
this.logger.info(`The '${this.collection}' collection is empty. Seeding the collection...`)
await this.seedDB()
this.logger.info('Seeding is done. Number of records:', await this.adapter.count())
}
}
},
}
}
public start () {
/* TODO: Consider using Model.validate on NEDB requests */
this.schema.adapter = this.makeAdapter({
uri: process.env.MONGO_URI,
isTest: !isNil(process.env.TEST) || process.env.NODE_ENV === 'test'
})
return this.schema
}
private ensureDevStorage () {
/* Create data folder */
if (!existsSync('./data')) {
sync('./data')
}
return `./data/${this.collection}.db`
}
private makeAdapter ({ uri, isTest }: {uri: string, isTest: boolean}) {
return uri ? new MongooseAdapter(uri, mongooseOptions) : new DbService.MemoryAdapter({
filename: isTest ? undefined : this.ensureDevStorage()
})
}
public get _collection(): string {
return this.collection
}
public set _collection(value: string) {
this.collection = value
}
}

View File

@ -0,0 +1,13 @@
import { google, GoogleApis, youtube_v3 } from "googleapis"
export default class GoogleServices{
public google: GoogleApis = google
private key: string = process.env.GOOGLE_KEY
public constructor () {
this.google.options({
auth: this.key
})
}
}

View File

@ -0,0 +1,23 @@
import { LiteratureList as ILiteratureList, LiteratureItem as ILiteratureItem } from '../../common/types/literature-list'
import { model, Schema, Document } from 'mongoose'
const LiteratureItemSchema: Record<keyof ILiteratureItem, any> = {
name: { type: String, required: true },
authors: { type: [String], required: true },
publicationYear: { type: Number, required: true },
publisher: { type: String, required: true }
}
const LiteratureListSchema: Record<keyof ILiteratureList, any> = {
name: { type: String, required: true },
description: { type: String, required: true },
literature: [{ type: Schema.Types.ObjectId, ref: 'LiteratureItem' }]
}
export interface LiteratureItemDocument extends Document<ILiteratureItem> {}
export interface LiteratureListDocument extends Document<ILiteratureList> {}
export const LiteratureItem = model<LiteratureItemDocument>('LiteratureItem', new Schema<LiteratureItemDocument>(LiteratureItemSchema))
export const LiteratureList = model<LiteratureListDocument>('LiteratureList', new Schema<LiteratureListDocument>(LiteratureListSchema))
export { default as mongoose } from 'mongoose'

View File

@ -0,0 +1,193 @@
'use strict';
import { BrokerOptions, Errors, MetricRegistry, ServiceBroker } from 'moleculer'
/**
* Moleculer ServiceBroker configuration file
*
* More info about options:
* https://moleculer.services/docs/0.14/configuration.html
*
*
* Overwriting options in production:
* ================================
* You can overwrite any option with environment variables.
* For example to overwrite the 'logLevel' value, use `LOGLEVEL=warn` env var.
* To overwrite a nested parameter, e.g. retryPolicy.retries, use `RETRYPOLICY_RETRIES=10` env var.
*
* To overwrite brokers deeply nested default options, which are not presented in 'moleculer.config.js',
* use the `MOL_` prefix and double underscore `__` for nested properties in .env file.
* For example, to set the cacher prefix to `MYCACHE`, you should declare an env var as `MOL_CACHER__OPTIONS__PREFIX=mycache`.
* It will set this:
* {
* cacher: {
* options: {
* prefix: 'mycache'
* }
* }
* }
*/
const brokerConfig: BrokerOptions = {
// Namespace of nodes to segment your nodes on the same network.
namespace: 'red-plateaus',
// Unique node identifier. Must be unique in a namespace.
nodeID: null,
// Custom metadata store. Store here what you want. Accessing: `this.broker.metadata`
metadata: {},
// Enable/disable logging or use custom logger. More info: https://moleculer.services/docs/0.14/logging.html
// Available logger types: 'Console', 'File', 'Pino', 'Winston', 'Bunyan', 'debug', 'Log4js', 'Datadog'
logger: {
type: 'Console',
options: {
// Using colors on the output
colors: true,
// Print module names with different colors (like docker-compose for containers)
moduleColors: false,
// Line formatter. It can be 'json', 'short', 'simple', 'full', a `Function` or a template string like '{timestamp} {level} {nodeID}/{mod}: {msg}'
formatter: 'full',
// Custom object printer. If not defined, it uses the `util.inspect` method.
objectPrinter: null,
// Auto-padding the module name in order to messages begin at the same column.
autoPadding: false,
},
},
// Default log level for built-in console logger. It can be overwritten in logger options above.
// Available values: trace, debug, info, warn, error, fatal
logLevel: 'info',
// Define transporter.
// More info: https://moleculer.services/docs/0.14/networking.html
// Note: During the development, you don't need to define it because all services will be loaded locally.
// In production you can set it via `TRANSPORTER=nats://localhost:4222` environment variable.
transporter: null,
// Define a cacher.
// More info: https://moleculer.services/docs/0.14/caching.html
cacher: {
type: 'Memory',
options: {
maxParamsLength: 60,
ttl: 60
}
},
// Define a serializer.
// Available values: 'JSON', 'Avro', 'ProtoBuf', 'MsgPack', 'Notepack', 'Thrift'.
// More info: https://moleculer.services/docs/0.14/networking.html#Serialization
serializer: 'Avro',
// Number of milliseconds to wait before reject a request with a RequestTimeout error. Disabled: 0
requestTimeout: 10 * 1000,
// Retry policy settings. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Retry
retryPolicy: {
// Enable feature
enabled: false,
// Count of retries
retries: 5,
// First delay in milliseconds.
delay: 100,
// Maximum delay in milliseconds.
maxDelay: 1000,
// Backoff factor for delay. 2 means exponential backoff.
factor: 2,
// A function to check failed requests.
check: (err: Errors.MoleculerError) => err && !!err.retryable,
},
// Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection)
maxCallLevel: 100,
// Number of seconds to send heartbeat packet to other nodes.
heartbeatInterval: 10,
// Number of seconds to wait before setting node to unavailable status.
heartbeatTimeout: 30,
// Cloning the params of context if enabled. High performance impact, use it with caution!
contextParamsCloning: false,
// Tracking requests and waiting for running requests before shuting down. More info: https://moleculer.services/docs/0.14/context.html#Context-tracking
tracking: {
// Enable feature
enabled: false,
// Number of milliseconds to wait before shuting down the process.
shutdownTimeout: 5000,
},
// Disable built-in request & emit balancer. (Transporter must support it, as well.). More info: https://moleculer.services/docs/0.14/networking.html#Disabled-balancer
disableBalancer: false,
// Settings of Service Registry. More info: https://moleculer.services/docs/0.14/registry.html
registry: {
// Define balancing strategy. More info: https://moleculer.services/docs/0.14/balancing.html
// Available values: 'RoundRobin', 'Random', 'CpuUsage', 'Latency', 'Shard'
strategy: 'RoundRobin',
// Enable local action call preferring. Always call the local action instance if available.
preferLocal: true,
},
// Settings of Circuit Breaker. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Circuit-Breaker
circuitBreaker: {
// Enable feature
enabled: false,
// Threshold value. 0.5 means that 50% should be failed for tripping.
threshold: 0.5,
// Minimum request count. Below it, CB does not trip.
minRequestCount: 20,
// Number of seconds for time window.
windowTime: 60,
// Number of milliseconds to switch from open to half-open state
halfOpenTime: 10 * 1000,
// A function to check failed requests.
check: (err: Errors.MoleculerError) => err && err.code >= 500,
},
// Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead
bulkhead: {
// Enable feature.
enabled: false,
// Maximum concurrent executions.
concurrency: 10,
// Maximum size of queue
maxQueueSize: 100,
},
// Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html
validator: true,
errorHandler: null,
// Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html
metrics: {
enabled: false,
// Available built-in reporters: 'Console', 'CSV', 'Event', 'Prometheus', 'Datadog', 'StatsD'
reporter: {
type: '',
},
},
// Enable built-in tracing function. More info: https://moleculer.services/docs/0.14/tracing.html
tracing: {
enabled: false,
// Available built-in exporters: 'Console', 'Datadog', 'Event', 'EventLegacy', 'Jaeger', 'Zipkin'
exporter: {
type: '', // Console exporter is only for development!
},
},
// Register custom middlewares
middlewares: [],
// Register custom REPL commands.
replCommands: null,
/*
// Called after broker created.
created : (broker: ServiceBroker): void => {},
// Called after broker started.
started: async (broker: ServiceBroker): Promise<void> => {},
stopped: async (broker: ServiceBroker): Promise<void> => {},
*/
};
export = brokerConfig;

29625
services/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

80
services/package.json Normal file
View File

@ -0,0 +1,80 @@
{
"name": "redplateaus",
"version": "1.0.0",
"description": "Red Plateaus microservices",
"scripts": {
"build": "tsc --build tsconfig.json",
"dev": "ts-node ./node_modules/moleculer/bin/moleculer-runner.js -e --hot --repl --config moleculer.config.ts services/**/*.service.ts",
"start": "moleculer-runner",
"cli": "moleculer connect ",
"ci": "jest --watch",
"test": "jest --coverage",
"lint": "eslint --ext .js,.ts .",
"dc:up": "docker-compose up --build -d",
"dc:logs": "docker-compose logs -f",
"dc:down": "docker-compose down"
},
"keywords": [
"microservices",
"moleculer"
],
"author": "",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"eslint": "^6.8.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prefer-arrow": "^1.2.2",
"jest": "^26.6.3",
"jest-cli": "^25.1.0",
"moleculer-repl": "^0.6.2",
"ts-jest": "^26.5.1",
"ts-node": "^8.8.1",
"ts-toolbelt": "^9.5.10"
},
"dependencies": {
"@admin-bro/express": "^3.1.0",
"@admin-bro/mongoose": "^1.1.0",
"@types/jest": "^25.1.4",
"@types/mkdirp": "^1.0.0",
"@types/node": "^13.9.8",
"@types/ramda": "^0.27.39",
"admin-bro": "^3.4.0",
"admin-bro-theme-dark": "^1.0.0",
"avsc": "^5.5.3",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"googleapis": "^67.0.0",
"moleculer": "^0.14.0",
"moleculer-db": "^0.8.4",
"moleculer-db-adapter-mongo": "^0.4.7",
"moleculer-db-adapter-mongoose": "^0.8.9",
"moleculer-web": "^0.10.0-beta1",
"ramda": "^0.27.1",
"shrink-ray-current": "^4.1.2",
"typescript": "4.1"
},
"engines": {
"node": ">= 10.x.x"
},
"jest": {
"coverageDirectory": "<rootDir>/coverage",
"testEnvironment": "node",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"testMatch": [
"**/*.spec.(ts|js)"
],
"globals": {
"ts-jest": {
"tsConfig": "tsconfig.json"
}
}
}
}

23
services/public/README.md Normal file
View File

@ -0,0 +1,23 @@
# Your Favicon Package
This package was generated with [RealFaviconGenerator](https://realfavicongenerator.net/) [v0.16](https://realfavicongenerator.net/change_log#v0.16)
## Install instructions
To install this package:
Extract this package in the root of your web site. If your site is <code>http://www.example.com</code>, you should be able to access a file named <code>http://www.example.com/favicon.ico</code>.
Insert the following code in the `head` section of your pages:
<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="#d32f2f">
<meta name="apple-mobile-web-app-title" content="Red Plateaus">
<meta name="application-name" content="Red Plateaus">
<meta name="msapplication-TileColor" content="#d32f2f">
<meta name="theme-color" content="#d32f2f">
*Optional* - Check your favicon with the [favicon checker](https://realfavicongenerator.net/favicon_checker)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
services/public/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#d32f2f</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

BIN
services/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

754
services/public/index.html Normal file
View File

@ -0,0 +1,754 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,minimal-ui">
<title>redplateaus - Moleculer Microservices Project</title>
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700" rel="stylesheet">
<link href="https://unpkg.com/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="shortcut icon" type="image/png" href="https://moleculer.services/icon/favicon-16x16.png"/>
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<style type="text/css">
html {
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
font-size: 18px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
padding: 0;
margin: 0;
color: black;
text-shadow: 1px 1px 3px rgba(0,0,0,0.2);
padding-bottom: 60px; /* footer */
}
.cursor-pointer {
cursor: pointer;
user-select: none;
}
header, footer {
text-align: center;
color: white;
text-shadow: 1px 1px 3px rgba(0,0,0,0.6);
}
header a, header a.router-link-exact-active,
footer a, footer a.router-link-exact-active
{
color: #63dcfd;
}
header {
background-image: linear-gradient(45deg, #e37682 0%, #5f4d93 100%);
padding: 1em;
box-shadow: 0 3px 10px rgba(0,0,0,0.6);
margin-bottom: 1em;
}
footer {
background-image: linear-gradient(135deg, #e37682 0%, #5f4d93 100%);
padding: 0.75em;
font-size: 0.8em;
box-shadow: 0 -3px 10px rgba(0,0,0,0.6);
position: fixed;
left: 0; right: 0; bottom: 0;
margin-top: 1em;
}
footer .footer-links {
margin-top: 0.5em;
}
footer .footer-links a {
margin: 0 0.5em;
}
a, a.router-link-exact-active {
color: #3CAFCE;
text-decoration: none;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
}
nav ul li {
display: inline-block;
padding: 0.25em 0.75em;
cursor: pointer;
font-weight: 300;
font-size: 1.25em;
border-bottom: 2px solid transparent;
transition: color .1s linear, border-bottom .1s linear;
}
nav ul li.active {
border-bottom: 2px solid #63dcfd;
}
nav ul li:hover {
color: #63dcfd;
}
button, .button {
background-color: #3CAFCE;
border: 0;
border-radius: 8px;
color: white;
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
font-size: 16px;
font-weight: 400;
padding: 0.5em 1em;
box-shadow: 0 4px 6px -1px rgba(0,0,0,.2);
cursor: pointer;
user-select: none;
}
button i, .button i {
margin-right: 0.5em;
}
button:hover, .button:hover {
background-color: #4ba3bb;
}
code {
font-family: "Consolas", 'Courier New', Courier, monospace;
color: #555;
}
main {
max-width: 1260px;
margin: 0 auto;
padding: 1em 1em;
}
main section#home > .content {
text-align: center;
}
main section#home h1 {
font-size: 2em;
font-weight: 400;
margin-top: 0;
}
main section#home h3 {
font-size: 1.25em;
font-weight: 600;
}
pre.broker-options {
display: inline-block;
text-align: left;
font-size: 0.9em;
}
.boxes {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.boxes .box {
width: 200px;
padding: 0.25em 1em;
margin: 0.5em;
background: rgba(60, 175, 206, 0.1);
border: 1px solid grey;
border-radius: 0.25em;
}
.boxes .box .caption {
font-weight: 300;
font-size: 0.9em;
margin-bottom: 0.5em;
}
.boxes .box .value {
font-weight: 600;
font-size: 1.1em;
}
main input {
border: 1px solid #3CAFCE;
border-radius: 4px;
padding: 2px 6px;
font-family: "Source Sans Pro";
}
main fieldset {
border: 1px solid lightgrey;
border-radius: 8px;
box-shadow: 2px 2px 10px rgba(0,0,0,0.4);
background-color: aliceblue;
margin-bottom: 2em;
}
main fieldset legend {
background-color: #cce7ff;
border: 1px solid lightgrey;
padding: 4px 10px;
border-radius: 8px;
}
main fieldset .content {
padding-left: 2em;
}
main fieldset .request {
margin-bottom: 0.5em;
}
main fieldset .parameters .field {
margin-bottom: 0.25em;
}
main fieldset .parameters .field label {
min-width: 80px;
display: inline-block;
text-align: right;
margin-right: 0.5em;
}
main fieldset .response {
margin-top: 1em;
}
main fieldset .response pre {
margin: 0.5em 0;
font-size: 0.9em;
}
pre.json .string { color: #885800; }
pre.json .number { color: blue; }
pre.json .boolean { color: magenta; }
pre.json .null { color: red; }
pre.json .key { color: green; }
main h4 {
font-weight: 600;
margin: 0.25em -1.0em;
}
.badge {
display: inline-block;
background-color: dimgray;
color: white;
padding: 2px 6px;
border-radius: 7px;
font-size: 0.7em;
font-weight: 600;
}
.badge.green {
background-color: limegreen;
}
.badge.red {
background-color: firebrick;
}
.badge.orange {
background-color: #fab000;
color: black;
}
table {
width: 100%;
/*max-width: 1000px;*/
border: 1px solid lightgrey;
border-radius: 8px;
background-color: aliceblue;
}
table th {
padding: 2px 4px;
background-color: #cce7ff;
border-radius: 4px;
}
table tr.offline td {
font-style: italic;
color: #777;
}
table tr.local td {
/*color: blue;*/
}
table tr:not(:last-child) td {
border-bottom: 1px solid #ddd;
}
table td {
text-align: center;
position: relative;
padding: 2px 4px;
}
table th:nth-child(1), table td:nth-child(1) {
text-align: left
}
table tr.service td:nth-child(1) {
font-weight: bold;
}
table tr.action td:nth-child(1) {
padding-left: 2em;
}
table tr td:nth-child(2) {
font-family: monospace;
font-size: 0.8em;
}
.bar {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
width: 0;
height: 100%;
background-color: rgba(0,0,0,0.3);
}
</style>
</head>
<body>
<div id="app">
<header>
<a href="https://moleculer.services/docs/0.14/" target="_blank"><img class="logo" src="https://moleculer.services/images/logo/logo_with_text_horizontal_100h_shadow.png" /></a>
<nav>
<ul>
<li v-for="item in menu" :class="{ active: page == item.id}" @click="changePage(item.id)">{{ item.caption }}</li>
</ul>
</nav>
</header>
<main>
<section id="home" v-if="page == 'home'">
<div class="content">
<h1>Welcome to your Moleculer microservices project!</h1>
<p>Check out the <a href="https://moleculer.services/docs/0.14/" target="_blank">Moleculer documentation</a> to learn how to customize this project.</p>
<template v-if="broker">
<h3>Configuration</h3>
<div class="boxes">
<div class="box">
<div class="caption">Namespace</div>
<div class="value">{{ broker.namespace || "&lt;not set&gt;" }}</div>
</div>
<div class="box">
<div class="caption">Transporter</div>
<div class="value">{{ broker.transporter || "&lt;no transporter&gt;" }}</div>
</div>
<div class="box">
<div class="caption">Serializer</div>
<div class="value">{{ broker.serializer || "JSON" }}</div>
</div>
<div class="box">
<div class="caption">Strategy</div>
<div class="value">{{ broker.registry.strategy || "Round Robin" }}</div>
</div>
<div class="box">
<div class="caption">Cacher</div>
<div class="value">{{ broker.cacher ? "Enabled" : "Disabled" }}</div>
</div>
<div class="box">
<div class="caption">Logger</div>
<div class="value">{{ broker.logger ? "Enabled" : "Disabled" }}</div>
</div>
<div class="box">
<div class="caption">Metrics</div>
<div class="value">{{ broker.metrics.enabled ? "Enabled" : "Disabled" }}</div>
</div>
<div class="box">
<div class="caption">Tracing</div>
<div class="value">{{ broker.tracing.enabled ? "Enabled" : "Disabled" }}</div>
</div>
</div>
<h3 class="cursor-pointer" @click="showBrokerOptions = !showBrokerOptions">Broker options <i :class="'fa fa-angle-' + (showBrokerOptions ? 'up' : 'down')"></i></h3>
<pre v-if="showBrokerOptions" class="broker-options"><code>{{ broker }}</code></pre>
</template>
</div>
</section>
<template v-for="(section, name) in requests">
<section :id="name" v-if="page == name">
<fieldset v-for="item in section">
<legend>
Action '<code>{{ item.action }}</code>'
</legend>
<div class="content">
<div class="request">
<h4>Request:</h4>
<code>{{ item.method || 'GET' }} <a target="_blank" :href="item.rest">{{ item.rest }} </a></code>
<a class="button" @click="callAction(item)">
<i class="fa fa-rocket"></i>
Execute
</a>
</div>
<div v-if="item.fields" class="parameters">
<h4>Parameters:</h4>
<div class="field" v-for="field in item.fields">
<label for="">{{ field.label }}: </label>
<input :type="field.type" :value="getFieldValue(field)" @input="setFieldValue(field, $event.target.value)"></input>
</div>
</div>
<div class="response" v-if="item.status">
<h4>Response:
<div class="badge" :class="{ green: item.status < 400, red: item.status >= 400 || item.status == 'ERR' }">{{ item.status }}</div>
<div class="badge time">{{ humanize(item.duration) }}</div>
</h4>
<pre><code>{{ item.response }}</code></pre>
</div>
</div>
</fieldset>
</section>
</template>
<section id="nodes" v-if="page == 'nodes'">
<table>
<thead>
<th>Node ID</th>
<th>Type</th>
<th>Version</th>
<th>IP</th>
<th>Hostname</th>
<th>Status</th>
<th>CPU</th>
</thead>
<tbody>
<tr v-for="node in nodes" :class="{ offline: !node.available, local: node.local }" :key="node.id">
<td>{{ node.id }}</td>
<td>{{ node.client.type }}</td>
<td>{{ node.client.version }}</td>
<td>{{ node.ipList[0] }}</td>
<td>{{ node.hostname }}</td>
<td><div class="badge" :class="{ green: node.available, red: !node.available }">{{ node.available ? "Online": "Offline" }}</div></td>
<td>
<div class="bar" :style="{ width: node.cpu != null ? node.cpu + '%' : '0' }"></div>
{{ node.cpu != null ? Number(node.cpu).toFixed(0) + '%' : '-' }}
</td>
</tr>
</tbody>
</table>
</section>
<section id="services" v-if="page == 'services'">
<table>
<thead>
<th>Service/Action name</th>
<th>REST</th>
<th>Parameters</th>
<th>Instances</th>
<th>Status</th>
</thead>
<tbody>
<template v-for="svc in filteredServices">
<tr class="service">
<td>
{{ svc.name }}
<div v-if="svc.version" class="badge">{{ svc.version }}</div>
</td>
<td>{{ svc.settings.rest ? svc.settings.rest : svc.fullName }}</td>
<td></td>
<td class="badges">
<div class="badge" v-for="nodeID in svc.nodes">
{{ nodeID }}
</div>
</td>
<td>
<div v-if="svc.nodes.length > 0" class="badge green">Online</div>
<div v-else class="badge red">Offline</div>
</td>
</tr>
<tr v-for="action in getServiceActions(svc)" :class="{ action: true, offline: !action.available, local: action.hasLocal }">
<td>
{{ action.name }}
<div v-if="action.action.cache" class="badge orange">cached</div>
</td>
<td v-html="getActionREST(svc, action)"></td>
<td :title="getActionParams(action)">
{{ getActionParams(action, 40) }}
</td>
<td></td>
<td>
<div v-if="action.available" class="badge green">Online</div>
<div v-else class="badge red">Offline</div>
</td>
</tr>
</template>
</tbody>
</table>
</section>
</main>
<footer>
<div class="footer-copyright">
Copyright &copy; 2016-2020 - Moleculer
</div>
<div class="footer-links">
<a href="https://github.com/moleculerjs/moleculer" class="footer-link" target="_blank">Github</a>
<a href="https://twitter.com/MoleculerJS" class="footer-link" target="_blank">Twitter</a>
<a href="https://discord.gg/TSEcDRP" class="footer-link" target="_blank">Discord</a>
<a href="https://stackoverflow.com/questions/tagged/moleculer" class="footer-link" target="_blank">Stack Overflow</a>
</div>
</footer>
</div>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data() {
return {
menu: [
{ id: "home", caption: "Home" },
{ id: "greeter", caption: "Greeter service" },
{ id: "products", caption: "Products service" },
{ id: "nodes", caption: "Nodes" },
{ id: "services", caption: "Services" }
],
page: "home",
requests: {
greeter: [
{ id: "hello", action: "greeter.hello", rest: "/api/greeter/hello", response: null, status: null, duration: null },
{ id: "welcome", action: "greeter.welcome", rest: "/api/greeter/welcome", fields: [
{ field: "name", label: "Name", type: "text", paramType: "param", model: "welcomeName" }
], response: null, status: null, duration: null }
],
products: [
{ id: "list", action: "products.list", rest: "/api/products/", response: null, status: null, duration: null, afterResponse: response => !this.fields.productID && (this.fields.productID = response.rows[0]._id) },
{ id: "create", action: "products.create", rest: "/api/products/", method: "POST", fields: [
{ field: "name", label: "Name", type: "text", paramType: "body", model: "productName" },
{ field: "price", label: "Price", type: "number", paramType: "body", model: "productPrice" },
], response: null, status: null, duration: null, afterResponse: response => this.fields.productID = response._id },
{ id: "get", action: "products.get", rest: "/api/products/:id", method: "GET", fields: [
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" }
], response: null, status: null, duration: null },
{ id: "update", action: "products.update", rest: "/api/products/:id", method: "PUT", fields: [
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" },
{ field: "name", label: "Name", type: "text", paramType: "body", model: "productName" },
{ field: "price", label: "Price", type: "number", paramType: "body", model: "productPrice" },
], response: null, status: null, duration: null },
{ id: "increase", action: "products.increaseQuantity", rest: "/api/products/:id/quantity/increase", method: "PUT", fields: [
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" },
{ field: "value", label: "Value", type: "number", paramType: "body", model: "productValue" },
], response: null, status: null, duration: null },
{ id: "decrease", action: "products.decreaseQuantity", rest: "/api/products/:id/quantity/decrease", method: "PUT", fields: [
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" },
{ field: "value", label: "Value", type: "number", paramType: "body", model: "productValue" },
], response: null, status: null, duration: null },
{ id: "delete", action: "products.delete", rest: "/api/products/:id", method: "DELETE", fields: [
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" }
], response: null, status: null, duration: null },
]
},
fields: {
welcomeName: "John",
productID: null,
productName: "Xiamoi Mi 9T",
productPrice: 299,
productValue: 1
},
broker: null,
nodes: [],
services: [],
actions: {},
showBrokerOptions: false
};
},
computed: {
filteredServices() {
return this.services.filter(svc => !svc.name.startsWith("$"));
}
},
methods: {
changePage(page) {
this.page = page;
this.updatePageResources();
},
humanize(ms) {
return ms > 1500 ? (ms / 1500).toFixed(2) + " s" : ms + " ms";
},
getFieldValue(field) {
return this.fields[field.model];
},
setFieldValue(field, newValue) {
if (field.type == "number")
this.fields[field.model] = Number(newValue);
else
this.fields[field.model] = newValue;
},
getServiceActions(svc) {
return Object.keys(svc.actions)
.map(name => this.actions[name])
.filter(action => !!action);
},
getActionParams(action, maxLen) {
if (action.action && action.action.params) {
const s = Object.keys(action.action.params).join(", ");
return s.length > maxLen ? s.substr(0, maxLen) + "…" : s;
}
return "-";
},
getActionREST(svc, action) {
if (action.action.rest) {
let prefix = svc.fullName || svc.name;
if (typeof(svc.settings.rest) == "string")
prefix = svc.settings.rest;
if (typeof action.action.rest == "string") {
if (action.action.rest.indexOf(" ") !== -1) {
const p = action.action.rest.split(" ");
return "<span class='badge'>" + p[0] + "</span> " + prefix + p[1];
} else {
return "<span class='badge'>*</span> " + prefix + action.action.rest;
}
} else {
return "<span class='badge'>" + (action.action.rest.method || "*") + "</span> " + prefix + action.action.rest.path;
}
}
return "";
},
callAction: function (item) {
var startTime = Date.now();
let url = item.rest;
const method = item.method || "GET";
let body = null;
let params = null;
if (item.fields) {
body = {};
params = {};
item.fields.forEach(field => {
const value = this.getFieldValue(field);
if (field.paramType == "body")
body[field.field] = value;
else if (field.paramType == "param")
params[field.field] = value;
else if (field.paramType == "url")
url = url.replace(":" + field.field, value);
});
if (body && method == "GET") {
body = null;
}
if (params) {
url += "?" + new URLSearchParams(params).toString();
}
}
return fetch(url, {
method,
body: body ? JSON.stringify(body) : null,
headers: {
'Content-Type': 'application/json'
}
}).then(function(res) {
item.status = res.status;
item.duration = Date.now() - startTime;
return res.json().then(json => {
item.response = json;
if (item.afterResponse)
return item.afterResponse(json);
});
}).catch(function (err) {
item.status = "ERR";
item.duration = Date.now() - startTime;
item.response = err.message;
console.log(err);
});
},
updateBrokerOptions: function (name) {
this.req("/api/~node/options", null).then(res => this.broker = res);
},
updateNodeList: function (name) {
this.req("/api/~node/list", null).then(res => {
res.sort((a,b) => a.id.localeCompare(b.id));
this.nodes = res;
});
},
updateServiceList: function (name) {
this.req("/api/~node/services?withActions=true", null)
.then(res => {
this.services = res;
res.sort((a,b) => a.name.localeCompare(b.name));
res.forEach(svc => svc.nodes.sort());
})
.then(() => this.req("/api/~node/actions", null))
.then(res => {
res.sort((a,b) => a.name.localeCompare(b.name));
const actions = res.reduce((a,b) => {
a[b.name] = b;
return a;
}, {});
Vue.set(this, "actions", actions);
});
},
req: function (url, params) {
return fetch(url, { method: "GET", body: params ? JSON.stringify(params) : null })
.then(function(res) {
return res.json();
});
},
updatePageResources() {
if (this.page == 'nodes') return this.updateNodeList();
if (this.page == 'services') return this.updateServiceList();
}
},
mounted() {
var self = this;
setInterval(function () {
self.updatePageResources();
}, 2000);
this.updateBrokerOptions();
}
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="3602.667" height="3602.667" viewBox="0 0 2702.000000 2702.000000"><path d="M1453.5 96.7c-9.9.5-30.2 3.3-43 5.9-93 19.2-171.6 73.3-218 149.9-25.1 41.4-42.1 91.8-50.3 149.1l-.7 5.1-9.5-5.4c-14.2-8.2-46.8-24.2-61.5-30.3-118.8-49.1-227.8-47.1-330.7 6.2-79.3 41.1-149.7 116.4-195.4 209.2-40.4 81.8-55.1 175.3-42.9 270.6.9 6.3 1.7 12.4 2 13.5.4 2 0 2-30.1 1.7-31.9-.4-49.9.5-76.4 3.9-78.9 9.9-158.1 39.1-225.5 83.1-9.8 6.4-40.9 35-55.9 51.5-65.2 71.6-100.9 156-112.2 265.3-2.1 21.2-3 83-1.5 109.8 4.9 85.7 14.7 147 34.6 215.2 22.6 77.6 59.3 158.7 109.7 242.8 30.9 51.6 96.6 150.5 136 204.8 99.9 137.5 218.7 253.1 355.3 345.6 104.7 70.9 218.1 128.1 328 165.4 171.9 58.4 374.3 61.3 596.5 8.7 190.2-45 390.2-132.7 554.5-243.2 57.9-38.9 110.7-79.3 157.5-120.5l7.5-6.6h266.4l1.9-5.3c1.1-2.8 33.4-86.7 71.7-186.2 38.4-99.6 70-181.8 70.2-182.7.5-1.7-3.4-1.8-68-2l-68.4-.3 3.9-10c36.2-92.8 56.6-182.6 65-287 2-24.5 1.7-100.7-.5-126.5-7.5-88.2-25.3-166.9-56.9-251.9-66.4-178.5-168.3-312.6-295.8-389.2-60.7-36.4-129.4-60.4-200.5-69.9-13.3-1.8-39.6-4-47.2-4-6.7 0-7.3-.2-7.7-2.3-.3-1.2-1.9-9.2-3.6-17.7-18.3-91.1-49.8-174.8-99-262.9-33.8-60.6-57.9-97.3-87.7-133.6-78.9-96.2-181.8-152.9-303-167-13-1.5-50.9-3.6-58.8-3.3-2.2.1-6.7.3-10 .5zm415.9 1765.5c-1.3 3.5-26.7 69.5-56.3 146.8l-54 140.5-63.6.3c-60 .2-63.6.3-63.1 2 .9 2.8 46.4 167.5 47.1 170.5l.7 2.7-88.3-.2-88.3-.3-25.6-87-25.6-87h-130.7l-33.5 87.2-33.5 87.3h-86.9c-82.4 0-87-.1-86.6-1.8.3-.9 35.7-93.5 78.8-205.7 43-112.2 78.5-204.8 78.7-205.7.4-1.1-6.7-10.6-21.1-28.3-11.9-14.6-21.6-26.8-21.6-27 0-.3 152.1-.5 337.9-.5h337.9l-2.4 6.2zm768.3 7c-2.8 7.3-29.4 76.5-59.2 153.8l-54.1 140.5-218.6.3c-207 .2-218.6.3-219.3 2-2.3 5.9-58.9 154.2-59.7 156.4l-1 2.8h-87c-82.4 0-87-.1-86.6-1.8.3-.9 35.7-93.5 78.8-205.7 43-112.2 78.5-204.8 78.7-205.7.4-1.1-6.7-10.6-21.1-28.3-11.9-14.6-21.6-26.8-21.6-27 0-.3 152.1-.5 337.9-.5h337.9l-5.1 13.2z"/><path d="M1398.6 1950.2c-5 12.1-41.6 107.7-41.6 108.6 0 .9 30.6 1.2 130.9 1.2h131l1.9-5.3c1.1-2.8 10.8-28 21.6-55.9s19.6-51 19.6-51.3c0-.3-59-.5-131.1-.5h-131l-1.3 3.2zM2147.1 2008.8c-12.9 33.9-23.6 62.3-23.9 63-.3 1 26 1.2 130.9 1l131.3-.3 23.3-61c12.8-33.6 23.6-61.8 23.9-62.8.5-1.6-6.2-1.7-130.8-1.7l-131.3.1-23.4 61.7z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,20 @@
{
"name": "Red Plateaus",
"short_name": "Red Plateaus",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#d32f2f",
"background_color": "#d32f2f",
"display": "standalone",
"orientation": "portrait"
}

View File

@ -0,0 +1,221 @@
import { IncomingMessage, Server, ServerResponse } from 'http'
import { Service, ServiceBroker, Context } from 'moleculer'
import ApiGateway from 'moleculer-web'
import express, { Application } from 'express'
/* Middleware */
import shrinkRay from 'shrink-ray-current'
import AdminBro from 'admin-bro'
import AdminBroExpress from '@admin-bro/express'
import AdminBroMongoose from '@admin-bro/mongoose'
import { theme } from '../util/admin.theme'
AdminBro.registerAdapter(AdminBroMongoose)
import mongoose from 'mongoose'
import { mongooseOptions } from '../util/mongoose.options'
/* Models */
import '../models/literature-list'
/* TODO: ts-env */
const PORT = process.env.PORT ?? 3000
export default class ApiService extends Service {
private app: Application
private server: Server
public constructor(broker: ServiceBroker) {
super(broker);
// @ts-ignore
this.parseServiceSchema({
name: 'api',
mixins: [ApiGateway],
/* More info about settings: https://moleculer.services/docs/0.14/moleculer-web.html */
settings: {
server: false, /* Disable integral web server */
routes: [{
whitelist: [
/* Access to all yt actions */
'youtube.*',
/* Read only access to anything that exposes DB operations wwwwwww*/
'*.list',
'*.find',
'*.get'
],
use: [
/* Only use this for things specific to moleculer api gateway */
],
mergeParams: true,
authentication: false,
authorization: false,
/*
* The auto-alias feature allows you to declare your route alias directly in your services.
* The gateway will dynamically build the full routes from service schema.
*/
autoAliases: true,
aliases:{},
// Calling options. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Calling-options
callingOptions: {},
bodyParsers: {
json: {
strict: false,
limit: '1MB',
},
urlencoded: {
extended: true,
limit: '1MB',
},
},
/* Mapping policy setting. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Mapping-policy */
mappingPolicy: 'all', // Available values: 'all', 'restrict'
/* Enable/disable logging */
logging: true,
}],
/* Do not log client side errors (does not log an error response when the error.code is 400<=X<500) */
log4XXResponses: false,
/* Logging the request parameters. Set to any log level to enable it. E.g. 'info' */
logRequestParams: null,
/* Logging the response data. Set to any log level to enable it. E.g. 'info' */
logResponseData: null
},
methods: {
/* NOTE: Leave these in place for easing any future expansion */
/* You can safely fold methods */
/**
* Authenticate the request. It check the `Authorization` token value in the request header.
* Check the token value & resolve the user by the token.
* The resolved user will be available in `ctx.meta.user`
*
* PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION!
*
* @param {Context} ctx
* @param {any} route
* @param {IncomingMessage} req
* @returns {Promise}
async authenticate = (ctx: Context, route: any, req: IncomingMessage): Promise < any > => {
// Read the token from header
const auth = req.headers.authorization;
if (auth && auth.startsWith('Bearer')) {
const token = auth.slice(7);
// Check the token. Tip: call a service which verify the token. E.g. `accounts.resolveToken`
if (token === '123456') {
// Returns the resolved user. It will be set to the `ctx.meta.user`
return {
id: 1,
name: 'John Doe',
};
} else {
// Invalid token
throw new ApiGateway.Errors.UnAuthorizedError(ApiGateway.Errors.ERR_INVALID_TOKEN, {
error: 'Invalid Token',
});
}
} else {
// No token. Throw an error or do nothing if anonymous access is allowed.
// Throw new E.UnAuthorizedError(E.ERR_NO_TOKEN);
return null;
}
},
*/
/**
* Authorize the request. Check that the authenticated user has right to access the resource.
*
* PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION!
*
* @param {Context} ctx
* @param {Object} route
* @param {IncomingMessage} req
* @returns {Promise}
async authorize = (ctx: Context < any, {
user: string;
} > , route: Record<string, undefined>, req: IncomingMessage): Promise < any > => {
// Get the authenticated user.
const user = ctx.meta.user;
// It check the `auth` property in action schema.
// @ts-ignore
if (req.$action.auth === 'required' && !user) {
throw new ApiGateway.Errors.UnAuthorizedError('NO_RIGHTS', {
error: 'Unauthorized',
});
}
},
*/
},
/* Mount lifecycle-events et al here */
created: this.created,
started: this.started,
stopped: this.stopped
})
}
async setupAdminBro () {
const db = await mongoose.connect(process.env.MONGO_URI, mongooseOptions)
const adminBro = new AdminBro({
databases: [db],
branding: {
companyName: 'Red Plateaus',
theme,
logo: '/RP_pote_tb.svg',
softwareBrothers: false
},
rootPath: '/admin'
})
return AdminBroExpress.buildRouter(adminBro)
}
created () {
this.app = express()
/* Remove things we don't need from here */
this.app.use(express.static('public'))
/* Compression */
this.app.use(shrinkRay())
/* Mount Moleculer service gateway*/
this.app.use('/api', this.express())
}
async started () {
if (process.env.MONGO_URI) {
/* Adminbro specifies we have to await Db connection, so */
this.app.use('/admin', await this.setupAdminBro())
}
/* Hello my friend, stay a while and */
this.server = this.app.listen(PORT, () => {
this.logger.info(`API Gateway listening on ${PORT}`)
})
}
async stopped () {
if (!this.server) this.logger.debug('Server was dead in the water')
this.server?.close() /* Server instance might not exist, prevent crashes */
}
}

View File

@ -0,0 +1,42 @@
import { Context, Service, ServiceBroker, ServiceSchema } from 'moleculer'
import DbConnection from '../mixins/db.mixin'
import { LiteratureItem } from '../models/literature-list'
export default class LiteratureItemService extends Service {
private DbMixin = new DbConnection(LiteratureItem).start()
// @ts-ignore
public constructor (public broker: ServiceBroker, schema: ServiceSchema<{}> = {}) {
super(broker)
this.parseServiceSchema(Service.mergeSchemas({
name: 'literature-item',
mixins: [this.DbMixin],
settings: {
fields: [
'_id',
'authors',
'name',
'publicationYear',
'publisher'
]
},
methods: {
async seedDB() {
await this.adapter.insertMany([
{
authors: [
'Friedrich Engels',
'Karl Marx'
],
name: 'Das Kapital',
publicationYear: 1867,
publisher: 'Verlag von Otto Meisner'
},
])
}
}
}, schema))
}
}

View File

@ -0,0 +1,40 @@
import { Context, Service, ServiceBroker, ServiceSchema } from 'moleculer'
import DbConnection from '../mixins/db.mixin'
import { LiteratureList } from '../models/literature-list'
export default class LiteratureListService extends Service {
private DbMixin = new DbConnection(LiteratureList).start()
// @ts-ignore
public constructor (public broker: ServiceBroker, schema: ServiceSchema<{}> = {}) {
super(broker)
this.parseServiceSchema(Service.mergeSchemas({
name: 'literature-list',
mixins: [this.DbMixin],
dependencies: [
'literature-item'
],
settings: {
fields: [
'_id',
'description',
'literature',
'name'
]
},
methods: {
async seedDB(this: Service) {
await this.adapter.insertMany([
{
description: 'Essential reading for all coomunists',
literature: await this.broker.call('literature-item.find'),
name: 'Coomunist Essentials'
},
])
}
}
}, schema))
}
}

View File

@ -0,0 +1,84 @@
'use strict'
import { youtube_v3 } from 'googleapis'
import { Context, Service, ServiceBroker, ServiceSchema } from 'moleculer'
import GoogleServices from '../mixins/google.mixin'
import { FeedResponse, FeedResult } from '../types/feed'
import { ApiMeta } from '../types/meta'
export default class YoutubeService extends Service {
private google = new GoogleServices().google
private channels: [channel: string, id: string][] = [
['Red Plateaus', 'UCsln1E-ttrNPsMivrnf9V7w'],
['Zoe Baker', 'UC3FD64RRsrCLpiZNkq7ZkSg'],
['Jonas Ceika,', 'UCSkzHxIcfoEr69MWBdo0ppg'],
['Radical Reviewer', 'UC_V9wKk1Dd2rpZ4fxj7pKXA'],
['donoteat01', 'UCFdazs-6CNzSVv1J0a-qy4A'],
['Contra', 'UCNvsIonJdJ5E4EXMa65VYpA'],
['Shaun', 'UCJ6o36XL0CpYb6U5dNBiXHQ'],
['hbomb', 'UClt01z1wHHT7c5lKcU8pxRQ'],
['Philo tube', 'UC2PA-AKmVpU6NKCGtZq_rKQ'],
['anark', 'UC1CjJYTUeor8EUFsbgwu5TQ']
]
// @ts-ignore
public constructor (public broker: ServiceBroker, schema: ServiceSchema<{}> = {}) {
super(broker)
this.parseServiceSchema(Service.mergeSchemas({
name: 'youtube',
actions: {
/**
* Get aggregate feed
*
*/
feed: {
cache: true,
rest: {
method: 'GET',
path: '/feed',
},
async handler (this: YoutubeService, ctx: Context<{}, ApiMeta>): Promise<FeedResponse> {
return this.GetAggregatedFeed()
}
}
}
}, schema))
}
public async GetAggregatedFeed (): Promise<FeedResponse> {
const onlyVideos = (item: youtube_v3.Schema$Activity) => item.snippet.type === 'upload'
return (await Promise.all(
this.channels.map(([channel, id]) => this.GetActivities(id))
))
.flatMap(response => response.data.items)
.filter(onlyVideos)
.map(item => this.PickActivityProperties(item))
}
private GetActivities (channelId: string) {
return this.google.youtube('v3').activities.list({
channelId,
part: ['snippet', 'contentDetails'],
maxResults: 1000
})
}
private PickBestThumbnail (thumbnails: youtube_v3.Schema$ThumbnailDetails) {
return String(Object.values(thumbnails).sort((a, b) => b.width - a.width)[0].url).split('/').pop()
/* https://i.ytimg.com/vi/${id}/${thumbnail} */
}
private PickActivityProperties (activity: youtube_v3.Schema$Activity): FeedResult {
return {
title: activity.snippet.title,
id: activity.contentDetails.upload.videoId,
description: activity.snippet.description?.substring(0, 127).split('\n')[0],
thumbnail: this.PickBestThumbnail(activity.snippet.thumbnails),
channel: activity.snippet.channelId,
date: new Date(activity.snippet.publishedAt).valueOf()
}
}
}

View File

@ -0,0 +1,115 @@
"use strict";
process.env.TEST = "true";
import { ServiceBroker } from "moleculer";
import TestService from "../../services/products.service";
describe("Test 'products' service", () => {
describe("Test actions", () => {
const broker = new ServiceBroker({ logger: false });
const service = broker.createService(TestService);
service.seedDB = null; // Disable seeding
beforeAll(() => broker.start());
afterAll(() => broker.stop());
const record = {
name: "Awesome item",
price: 999,
};
let newID: string;
it("should contains the seeded items", async () => {
const res = await broker.call("products.list");
expect(res).toEqual({ page: 1, pageSize: 10, rows: [], total: 0, totalPages: 0 });
});
it("should add the new item", async () => {
const res: any = await broker.call("products.create", record);
expect(res).toEqual({
_id: expect.any(String),
name: "Awesome item",
price: 999,
quantity: 0,
});
newID = res._id;
const res2 = await broker.call("products.count");
expect(res2).toBe(1);
});
it("should get the saved item", async () => {
const res = await broker.call("products.get", { id: newID });
expect(res).toEqual({
_id: expect.any(String),
name: "Awesome item",
price: 999,
quantity: 0,
});
const res2 = await broker.call("products.list");
expect(res2).toEqual({
page: 1,
pageSize: 10,
rows: [{ _id: newID, name: "Awesome item", price: 999, quantity: 0 }],
total: 1,
totalPages: 1,
});
});
it("should update an item", async () => {
const res = await broker.call("products.update", { id: newID, price: 499 });
expect(res).toEqual({
_id: expect.any(String),
name: "Awesome item",
price: 499,
quantity: 0,
});
});
it("should get the updated item", async () => {
const res = await broker.call("products.get", { id: newID });
expect(res).toEqual({
_id: expect.any(String),
name: "Awesome item",
price: 499,
quantity: 0,
});
});
it("should increase the quantity", async () => {
const res = await broker.call("products.increaseQuantity", { id: newID, value: 5 });
expect(res).toEqual({
_id: expect.any(String),
name: "Awesome item",
price: 499,
quantity: 5,
});
});
it("should decrease the quantity", async () => {
const res = await broker.call("products.decreaseQuantity", { id: newID, value: 2 });
expect(res).toEqual({
_id: expect.any(String),
name: "Awesome item",
price: 499,
quantity: 3,
});
});
it("should remove the updated item", async () => {
const res = await broker.call("products.remove", { id: newID });
expect(res).toBe(1);
const res2 = await broker.call("products.count");
expect(res2).toBe(0);
const res3 = await broker.call("products.list");
expect(res3).toEqual({ page: 1, pageSize: 10, rows: [], total: 0, totalPages: 0 });
});
});
});

View File

@ -0,0 +1,87 @@
"use strict";
process.env.TEST = "true";
import { ServiceBroker } from "moleculer";
import DbService from "moleculer-db";
import DbMixin from "../../../mixins/db.mixin";
describe("Test DB mixin", () => {
describe("Test schema generator", () => {
const broker = new ServiceBroker({ logger: false, cacher: "Memory" });
beforeAll(() => broker.start());
afterAll(() => broker.stop());
it("check schema properties", async () => {
const schema = new DbMixin("my-collection").start();
expect(schema.mixins).toEqual([DbService]);
// @ts-ignore
expect(schema.adapter).toBeInstanceOf(DbService.MemoryAdapter);
expect(schema.started).toBeDefined();
expect(schema.events["cache.clean.my-collection"]).toBeInstanceOf(Function);
});
it("check cache event handler", async () => {
jest.spyOn(broker.cacher, "clean");
const schema = new DbMixin("my-collection").start();
// @ts-ignore
await schema.events["cache.clean.my-collection"].call({ broker, fullName: "my-service" });
expect(broker.cacher.clean).toBeCalledTimes(1);
expect(broker.cacher.clean).toBeCalledWith("my-service.*");
});
describe("Check service started handler", () => {
it("should not call seedDB method", async () => {
const schema = new DbMixin("my-collection").start();
schema.adapter.count = jest.fn(async () => 10);
const seedDBFn = jest.fn();
// @ts-ignore
await schema.started.call({ broker, logger: broker.logger, adapter: schema.adapter, seedDB: seedDBFn });
expect(schema.adapter.count).toBeCalledTimes(1);
expect(schema.adapter.count).toBeCalledWith();
expect(seedDBFn).toBeCalledTimes(0);
});
it("should call seedDB method", async () => {
const schema = new DbMixin("my-collection").start();
schema.adapter.count = jest.fn(async () => 0);
const seedDBFn = jest.fn();
// @ts-ignore
await schema.started.call({ broker, logger: broker.logger, adapter: schema.adapter, seedDB: seedDBFn });
expect(schema.adapter.count).toBeCalledTimes(2);
expect(schema.adapter.count).toBeCalledWith();
expect(seedDBFn).toBeCalledTimes(1);
expect(seedDBFn).toBeCalledWith();
});
});
it("should broadcast a cache clear event", async () => {
const schema = new DbMixin("my-collection").start();
const ctx = {
broadcast: jest.fn(),
};
await schema.methods.entityChanged(null, null, ctx);
expect(ctx.broadcast).toBeCalledTimes(1);
expect(ctx.broadcast).toBeCalledWith("cache.clean.my-collection");
});
});
});

View File

@ -0,0 +1,40 @@
"use strict";
import { Errors, ServiceBroker} from "moleculer";
import TestService from "../../../services/greeter.service";
describe("Test 'greeter' service", () => {
const broker = new ServiceBroker({ logger: false });
broker.createService(TestService);
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'greeter.hello' action", () => {
it("should return with 'Hello Moleculer'", async () => {
const res = await broker.call("greeter.hello");
expect(res).toBe("Hello Moleculer");
});
});
describe("Test 'greeter.welcome' action", () => {
it("should return with 'Welcome'", async () => {
const res = await broker.call("greeter.welcome", { name: "Adam" });
expect(res).toBe("Welcome, Adam");
});
it("should reject an ValidationError", async () => {
expect.assertions(1);
try {
await broker.call("greeter.welcome");
} catch (err) {
expect(err).toBeInstanceOf(Errors.ValidationError);
}
});
});
});

View File

@ -0,0 +1,182 @@
"use strict";
process.env.TEST = "true";
import { Context, Errors, ServiceBroker } from "moleculer";
import TestService from "../../../services/products.service";
describe("Test 'products' service", () => {
describe("Test actions", () => {
const broker = new ServiceBroker({ logger: false });
const service = broker.createService(TestService);
jest.spyOn(service.adapter, "updateById");
jest.spyOn(service, "transformDocuments");
jest.spyOn(service, "entityChanged");
beforeAll(() => broker.start());
afterAll(() => broker.stop());
const record = {
_id: "123",
name: "Awesome thing",
price: 999,
quantity: 25,
createdAt: Date.now(),
};
describe("Test 'products.increaseQuantity'", () => {
it("should call the adapter updateById method & transform result", async () => {
service.adapter.updateById.mockImplementation(async () => record);
service.transformDocuments.mockClear();
service.entityChanged.mockClear();
const res = await broker.call("products.increaseQuantity", {
id: "123",
value: 10,
});
expect(res).toEqual({
_id: "123",
name: "Awesome thing",
price: 999,
quantity: 25,
});
expect(service.adapter.updateById).toBeCalledTimes(1);
expect(service.adapter.updateById).toBeCalledWith("123", { $inc: { quantity: 10 } } );
expect(service.transformDocuments).toBeCalledTimes(1);
expect(service.transformDocuments).toBeCalledWith(expect.any(Context), { id: "123", value: 10 }, record);
expect(service.entityChanged).toBeCalledTimes(1);
expect(service.entityChanged).toBeCalledWith("updated", { _id: "123", name: "Awesome thing", price: 999, quantity: 25 }, expect.any(Context));
});
});
describe("Test 'products.decreaseQuantity'", () => {
it("should call the adapter updateById method & transform result", async () => {
service.adapter.updateById.mockClear();
service.transformDocuments.mockClear();
service.entityChanged.mockClear();
const res = await broker.call("products.decreaseQuantity", {
id: "123",
value: 10,
});
expect(res).toEqual({
_id: "123",
name: "Awesome thing",
price: 999,
quantity: 25,
});
expect(service.adapter.updateById).toBeCalledTimes(1);
expect(service.adapter.updateById).toBeCalledWith("123", { $inc: { quantity: -10 } } );
expect(service.transformDocuments).toBeCalledTimes(1);
expect(service.transformDocuments).toBeCalledWith(expect.any(Context), { id: "123", value: 10 }, record);
expect(service.entityChanged).toBeCalledTimes(1);
expect(service.entityChanged).toBeCalledWith("updated", { _id: "123", name: "Awesome thing", price: 999, quantity: 25 }, expect.any(Context));
});
it("should throw error if params is not valid", async () => {
service.adapter.updateById.mockClear();
service.transformDocuments.mockClear();
service.entityChanged.mockClear();
expect.assertions(2);
try {
await broker.call("products.decreaseQuantity", {
id: "123",
value: -5,
});
} catch (err) {
expect(err).toBeInstanceOf(Errors.ValidationError);
expect(err.data).toEqual([{
action: "products.decreaseQuantity",
actual: -5,
field: "value",
message: "The 'value' field must be a positive number.",
nodeID: broker.nodeID,
type: "numberPositive",
}]);
}
});
});
});
describe("Test methods", () => {
const broker = new ServiceBroker({ logger: false });
const service = broker.createService(TestService);
jest.spyOn(service.adapter, "insertMany");
jest.spyOn(service, "seedDB");
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test 'seedDB'", () => {
it("should be called after service started & DB connected", async () => {
expect(service.seedDB).toBeCalledTimes(1);
expect(service.seedDB).toBeCalledWith();
});
it("should insert 3 documents", async () => {
expect(service.adapter.insertMany).toBeCalledTimes(1);
expect(service.adapter.insertMany).toBeCalledWith([
{ name: "Samsung Galaxy S10 Plus", quantity: 10, price: 704 },
{ name: "iPhone 11 Pro", quantity: 25, price: 999 },
{ name: "Huawei P30 Pro", quantity: 15, price: 679 },
]);
});
});
});
describe("Test hooks", () => {
const broker = new ServiceBroker({ logger: false });
const createActionFn = jest.fn();
// @ts-ignore
broker.createService(TestService, {
actions: {
create: {
handler: createActionFn,
},
},
});
beforeAll(() => broker.start());
afterAll(() => broker.stop());
describe("Test before 'create' hook", () => {
it("should add quantity with zero", async () => {
await broker.call("products.create", {
id: "111",
name: "Test product",
price: 100,
});
expect(createActionFn).toBeCalledTimes(1);
expect(createActionFn.mock.calls[0][0].params).toEqual({
id: "111",
name: "Test product",
price: 100,
quantity: 0,
});
});
});
});
});

19
services/tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true,
"skipLibCheck": true,
"pretty": true,
"target": "es2019",
"outDir": "dist"
},
"include": ["./**/*"],
"exclude": [
"node_modules/**/*",
"test"
]
}

9
services/types/feed.ts Normal file
View File

@ -0,0 +1,9 @@
export type FeedResponse = FeedResult[]
export interface FeedResult {
title: string
id: string,
description: string
thumbnail: string
channel: string
date: number
}

3
services/types/meta.ts Normal file
View File

@ -0,0 +1,3 @@
export interface ApiMeta {
$responseType: 'application/json'
}

View File

@ -0,0 +1,128 @@
import { BrandingOptions } from 'admin-bro'
export const theme: BrandingOptions['theme'] = {
VariantValues: [
'primary',
'danger',
'success',
'info',
'secondary',
'default',
'light',
],
colors: {
primary100: '#4268F6',
primary80: '#6483F8',
primary60: '#879FFA',
primary40: '#A9BAFA',
primary20: '#CBD5FD',
accent: '#38CAF1',
love: '#e6282b',
grey100: '#F6F7FB',
grey80: '#A9AABC',
grey60: '#898A9A',
grey40: '#C0C0CA',
grey20: '#303B62',
white: '#192035',
errorDark: '#DE405D',
error: '#FF4567',
errorLight: '#660040',
successDark: '#32A887',
success: '#70C9B0',
successLight: '#008340',
infoDark: '#4268F6',
info: '#879FFA',
infoLight: '#CBD5FD',
filterBg: '#898A9A',
hoverBg: '#535B8E',
/* border: '#DDE1E5', */
inputBorder: '#A9AABC',
separator: '#A9AABC',
highlight: '#20273E',
filterInputBorder: 'rgba(255,255,255,0.8)',
filterDisabled: 'rgba(83,91,142,0.05)',
bg: '#20273E',
defaultText: '#FFFFFF',
lightText: '#A9AABC',
border: '#2E324A',
borderOnDark: '#2E324A',
innerBck: '#192035',
darkBck: '#20273E',
lightBck: '#485899',
superLightBack: '#303B62',
inputBck: '#192035',
lightSuccess: '#008340',
lightError: '#660040',
},
lineHeights: {
xs: '10px',
sm: '12px',
default: '16px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '40px',
},
fontWeights: {
// @ts-ignore
lighter: 200,
// @ts-ignore
light: 300,
// @ts-ignore
normal: 400,
// @ts-ignore
bold: 500,
// @ts-ignore
bolder: 900,
},
fontSizes: {
xs: '10px',
sm: '12px',
default: '14px',
md: '14px',
lg: '16px',
xl: '18px',
h4: '24px',
h3: '28px',
h2: '32px',
h1: '40px',
},
sizes: {
navbarHeight: '64px',
sidebarWidth: '300px',
maxFormWidth: '740px',
},
space: {
xs: '2px',
sm: '4px',
default: '8px',
md: '8px',
lg: '16px',
xl: '24px',
xxl: '32px',
x3: '48px',
x4: '64px',
x5: '80px',
x6: '128px',
},
// @ts-ignore
font: "'Roboto', sans-serif",
shadows: {
login: '0 15px 24px 0 rgba(137,138,154,0.15)',
cardHover: '0 4px 12px 0 rgba(137,138,154,0.4)',
drawer: '-2px 0 8px 0 rgba(137,138,154,0.2)',
card: '0 1px 6px 0 rgba(137,138,154,0.4)',
inputFocus: '0 2px 4px 0 rgba(135,159,250,0.4)',
buttonFocus: '0 4px 6px 0 rgba(56,202,241,0.3)',
},
borders: {
input: '1px solid #C0C0CA',
filterInput: ' 1px rgba(255,255,255,0. solid15)',
bg: '1px solid #20273E',
default: '1px solid #2E324A',
},
breakpoints: ['577px', '769px', '1024px', '1324px'],
borderWidths: {
default: '0px',
}
}

View File

@ -0,0 +1,8 @@
import { ConnectOptions } from 'mongoose'
export const mongooseOptions: ConnectOptions = {
useUnifiedTopology: true,
useCreateIndex: true,
useNewUrlParser: true,
autoIndex: true
}