first commit
12
.editorconfig
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
12
common/types/literature-list.ts
Normal 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[]
|
||||||
|
}
|
4
common/types/rest-response.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export interface RestResponse<T> {
|
||||||
|
status: number
|
||||||
|
|
||||||
|
}
|
15
red-plateaus.code-workspace
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "Root",
|
||||||
|
"path": "."
|
||||||
|
}, {
|
||||||
|
"name": "Services",
|
||||||
|
"path": "services"
|
||||||
|
}, {
|
||||||
|
"name": "Common",
|
||||||
|
"path": "common"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
6
services/.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
docker-compose.env
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
node_modules
|
||||||
|
test
|
||||||
|
.vscode
|
35
services/.editorconfig
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
9
services/docker-compose.env
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
NAMESPACE=
|
||||||
|
LOGGER=true
|
||||||
|
LOGLEVEL=info
|
||||||
|
SERVICEDIR=dist/services
|
||||||
|
CONFIG=dist
|
||||||
|
|
||||||
|
CACHER=Memory
|
||||||
|
|
||||||
|
MONGO_URI=mongodb://mongo/redplateaus
|
68
services/docker-compose.yml
Normal 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
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
97
services/mixins/db.mixin.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
13
services/mixins/google.mixin.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
23
services/models/literature-list.ts
Normal 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'
|
193
services/moleculer.config.ts
Normal 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 broker’s 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
80
services/package.json
Normal 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
|
@ -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)
|
26
services/public/RP_pote_tb.svg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
services/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
services/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
services/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
services/public/banner.png
Normal file
After Width: | Height: | Size: 240 KiB |
9
services/public/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#d32f2f</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
services/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 466 B |
BIN
services/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 753 B |
BIN
services/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
754
services/public/index.html
Normal 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 || "<not set>" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Transporter</div>
|
||||||
|
<div class="value">{{ broker.transporter || "<no transporter>" }}</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 © 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>
|
BIN
services/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
1
services/public/safari-pinned-tab.svg
Normal 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 |
20
services/public/site.webmanifest
Normal 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"
|
||||||
|
}
|
221
services/services/api.service.ts
Normal 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 */
|
||||||
|
}
|
||||||
|
}
|
42
services/services/literature-item.service.ts
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
40
services/services/literature-list.service.ts
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
84
services/services/youtube.service.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
services/test/integration/products.service.spec.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
87
services/test/unit/mixins/db.mixin.spec.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
40
services/test/unit/services/greeter.spec.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
182
services/test/unit/services/products.spec.ts
Normal 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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
||||||
|
export interface ApiMeta {
|
||||||
|
$responseType: 'application/json'
|
||||||
|
}
|
128
services/util/admin.theme.ts
Normal 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',
|
||||||
|
}
|
||||||
|
}
|
8
services/util/mongoose.options.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { ConnectOptions } from 'mongoose'
|
||||||
|
|
||||||
|
export const mongooseOptions: ConnectOptions = {
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
useCreateIndex: true,
|
||||||
|
useNewUrlParser: true,
|
||||||
|
autoIndex: true
|
||||||
|
}
|