🥇 First commit
This commit is contained in:
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
docker-compose.env
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
node_modules
|
||||||
|
test
|
||||||
|
.vscode
|
||||||
35
.editorconfig
Normal file
35
.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 = tab
|
||||||
|
indent_size = 4
|
||||||
|
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]
|
||||||
|
quote_type = "double"
|
||||||
143
.eslintrc.js
Normal file
143
.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",
|
||||||
|
}
|
||||||
|
};
|
||||||
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# 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/
|
||||||
|
.vscode
|
||||||
19
Dockerfile
Normal file
19
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 ["npm", "start"]
|
||||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
[](https://moleculer.services)
|
||||||
|
|
||||||
|
# comicvine-service
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Services
|
||||||
|
- **api**: API Gateway services
|
||||||
|
- **greeter**: Sample service with `hello` and `welcome` actions.
|
||||||
|
|
||||||
|
|
||||||
|
## 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
docker-compose.env
Normal file
9
docker-compose.env
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
NAMESPACE=
|
||||||
|
LOGGER=true
|
||||||
|
LOGLEVEL=info
|
||||||
|
SERVICEDIR=dist/services
|
||||||
|
|
||||||
|
TRANSPORTER=nats://nats:4222
|
||||||
|
|
||||||
|
CACHER=Memory
|
||||||
|
|
||||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
version: "3.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
image: comicvine-service
|
||||||
|
env_file: docker-compose.env
|
||||||
|
environment:
|
||||||
|
SERVICES: api
|
||||||
|
PORT: 3000
|
||||||
|
depends_on:
|
||||||
|
- nats
|
||||||
|
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: comicvine-service
|
||||||
|
env_file: docker-compose.env
|
||||||
|
environment:
|
||||||
|
SERVICES: greeter
|
||||||
|
depends_on:
|
||||||
|
- nats
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
nats:
|
||||||
|
image: nats:2
|
||||||
|
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:
|
||||||
164
k8s.yaml
Normal file
164
k8s.yaml
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#########################################################
|
||||||
|
# Common Environment variables ConfigMap
|
||||||
|
#########################################################
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: common-env
|
||||||
|
data:
|
||||||
|
NAMESPACE: ""
|
||||||
|
LOGLEVEL: info
|
||||||
|
SERVICEDIR: dist/services
|
||||||
|
TRANSPORTER: nats://nats:4222
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CACHER: Memory
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
#########################################################
|
||||||
|
# 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: comicvine-service.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: comicvine-service
|
||||||
|
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: comicvine-service
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: common-env
|
||||||
|
env:
|
||||||
|
- name: SERVICES
|
||||||
|
value: greeter
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
#########################################################
|
||||||
|
# NATS transporter service
|
||||||
|
#########################################################
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nats
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: nats
|
||||||
|
ports:
|
||||||
|
- port: 4222
|
||||||
|
name: nats
|
||||||
|
targetPort: 4222
|
||||||
|
|
||||||
|
---
|
||||||
|
#########################################################
|
||||||
|
# NATS transporter
|
||||||
|
#########################################################
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nats
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nats
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nats
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nats
|
||||||
|
image: nats
|
||||||
|
ports:
|
||||||
|
- containerPort: 4222
|
||||||
|
name: nats
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
208
moleculer.config.ts
Normal file
208
moleculer.config.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
"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: "",
|
||||||
|
// 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, // "NATS"
|
||||||
|
|
||||||
|
// Define a cacher.
|
||||||
|
// More info: https://moleculer.services/docs/0.14/caching.html
|
||||||
|
cacher: "Memory",
|
||||||
|
|
||||||
|
// Define a serializer.
|
||||||
|
// Available values: "JSON", "Avro", "ProtoBuf", "MsgPack", "Notepack", "Thrift".
|
||||||
|
// More info: https://moleculer.services/docs/0.14/networking.html#Serialization
|
||||||
|
serializer: "JSON",
|
||||||
|
|
||||||
|
// 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: true,
|
||||||
|
// Available built-in reporters: "Console", "CSV", "Event", "Prometheus", "Datadog", "StatsD"
|
||||||
|
reporter: {
|
||||||
|
type: "Console",
|
||||||
|
options: {
|
||||||
|
// HTTP port
|
||||||
|
port: 3030,
|
||||||
|
// HTTP URL path
|
||||||
|
path: "/metrics",
|
||||||
|
// Default labels which are appended to all metrics labels
|
||||||
|
defaultLabels: (registry: MetricRegistry) => ({
|
||||||
|
namespace: registry.broker.namespace,
|
||||||
|
nodeID: registry.broker.nodeID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable built-in tracing function. More info: https://moleculer.services/docs/0.14/tracing.html
|
||||||
|
tracing: {
|
||||||
|
enabled: true,
|
||||||
|
// Available built-in exporters: "Console", "Datadog", "Event", "EventLegacy", "Jaeger", "Zipkin"
|
||||||
|
exporter: {
|
||||||
|
type: "Console", // Console exporter is only for development!
|
||||||
|
options: {
|
||||||
|
// Custom logger
|
||||||
|
logger: null,
|
||||||
|
// Using colors
|
||||||
|
colors: true,
|
||||||
|
// Width of row
|
||||||
|
width: 100,
|
||||||
|
// Gauge width in the row
|
||||||
|
gaugeWidth: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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;
|
||||||
8718
package-lock.json
generated
Normal file
8718
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
package.json
Normal file
68
package.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "comicvine-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "My Moleculer-based microservices project",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build tsconfig.json",
|
||||||
|
"dev": "ts-node ./node_modules/moleculer/bin/moleculer-runner.js --hot --repl --config moleculer.config.ts services/**/*.service.ts",
|
||||||
|
"start": "moleculer-runner --config dist/moleculer.config.js",
|
||||||
|
"cli": "moleculer connect NATS",
|
||||||
|
"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",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-import": "^2.20.2",
|
||||||
|
"eslint-plugin-prefer-arrow": "^1.2.2",
|
||||||
|
"jest": "^25.1.0",
|
||||||
|
"jest-cli": "^25.1.0",
|
||||||
|
"moleculer-repl": "^0.6.2",
|
||||||
|
"threetwo-ui-typings": "^1.0.1-0",
|
||||||
|
"ts-jest": "^25.3.0",
|
||||||
|
"ts-node": "^8.8.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jest": "^25.1.4",
|
||||||
|
"@types/mkdirp": "^1.0.0",
|
||||||
|
"@types/node": "^13.9.8",
|
||||||
|
"moleculer": "^0.14.0",
|
||||||
|
"moleculer-web": "^0.9.0",
|
||||||
|
"nats": "^1.3.2",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/banner.png
Normal file
BIN
public/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
716
public/index.html
Normal file
716
public/index.html
Normal file
@@ -0,0 +1,716 @@
|
|||||||
|
|
||||||
|
<!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>comicvine-service - 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: "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 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
welcomeName: "John"
|
||||||
|
},
|
||||||
|
|
||||||
|
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>
|
||||||
174
services/api.service.ts
Normal file
174
services/api.service.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import {IncomingMessage} from "http";
|
||||||
|
import {Service, ServiceBroker, Context} from "moleculer";
|
||||||
|
import ApiGateway from "moleculer-web";
|
||||||
|
|
||||||
|
export default class ApiService extends Service {
|
||||||
|
|
||||||
|
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: {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
|
||||||
|
routes: [{
|
||||||
|
path: "/api",
|
||||||
|
whitelist: [
|
||||||
|
// Access to any actions in all services under "/api" URL
|
||||||
|
"**",
|
||||||
|
],
|
||||||
|
// Route-level Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares
|
||||||
|
use: [],
|
||||||
|
// Enable/disable parameter merging method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Disable-merging
|
||||||
|
mergeParams: true,
|
||||||
|
|
||||||
|
// Enable authentication. Implement the logic into `authenticate` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authentication
|
||||||
|
authentication: false,
|
||||||
|
|
||||||
|
// Enable authorization. Implement the logic into `authorize` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authorization
|
||||||
|
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:{},
|
||||||
|
/**
|
||||||
|
* Before call hook. You can check the request.
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {Object} route
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @param {Object} data
|
||||||
|
onBeforeCall(ctx: Context<any,{userAgent: string}>,
|
||||||
|
route: object, req: IncomingMessage, res: ServerResponse) {
|
||||||
|
Set request headers to context meta
|
||||||
|
ctx.meta.userAgent = req.headers["user-agent"];
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After call hook. You can modify the data.
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {Object} route
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @param {Object} data
|
||||||
|
*
|
||||||
|
onAfterCall(ctx: Context, route: object, req: IncomingMessage, res: ServerResponse, data: object) {
|
||||||
|
// Async function which return with Promise
|
||||||
|
return doSomething(ctx, res, data);
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
// Serve assets from "public" folder
|
||||||
|
assets: {
|
||||||
|
folder: "public",
|
||||||
|
// Options to `server-static` module
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate the request. It checks 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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
62
services/comicvine.service.ts
Normal file
62
services/comicvine.service.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { Service, ServiceBroker, Context } from "moleculer";
|
||||||
|
import axios from "axios";
|
||||||
|
const CV_BASE_URL = "https://comicvine.gamespot.com/api/";
|
||||||
|
const CV_API_KEY = "a5fa0663683df8145a85d694b5da4b87e1c92c69";
|
||||||
|
|
||||||
|
export default class GreeterService extends Service {
|
||||||
|
public constructor(public broker: ServiceBroker) {
|
||||||
|
super(broker);
|
||||||
|
this.parseServiceSchema({
|
||||||
|
name: "comicvine",
|
||||||
|
actions: {
|
||||||
|
fetchSeries: {
|
||||||
|
rest: "/fetchseries",
|
||||||
|
params: {
|
||||||
|
format: { type: "string", optional: false },
|
||||||
|
sort: { type: "string", optional: true },
|
||||||
|
query: { type: "string", optional: false },
|
||||||
|
fieldList: { type: "string", optional: true },
|
||||||
|
limit: { type: "string", optional: false },
|
||||||
|
offset: { type: "string", optional: false },
|
||||||
|
resources: { type: "string", optional: false },
|
||||||
|
},
|
||||||
|
handler: async (
|
||||||
|
ctx: Context<{
|
||||||
|
format: string;
|
||||||
|
sort: string;
|
||||||
|
query: string;
|
||||||
|
fieldList: string;
|
||||||
|
limit: string;
|
||||||
|
offset: string;
|
||||||
|
resources: string;
|
||||||
|
}>
|
||||||
|
): Promise<any> => {
|
||||||
|
const response = await axios.request({
|
||||||
|
url:
|
||||||
|
CV_BASE_URL +
|
||||||
|
"search" +
|
||||||
|
"?api_key=" +
|
||||||
|
CV_API_KEY,
|
||||||
|
params: ctx.params,
|
||||||
|
transformResponse: (r) => JSON.parse(r),
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
});
|
||||||
|
const { data } = response;
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action
|
||||||
|
public ActionHello(): string {
|
||||||
|
return "Hello Moleculer";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionWelcome(name: string): string {
|
||||||
|
return `Welcome, ${name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
85
test/unit/mixins/db.mixin.spec.ts
Normal file
85
test/unit/mixins/db.mixin.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
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
test/unit/services/greeter.spec.ts
Normal file
40
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"pretty": true,
|
||||||
|
"target": "es6",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["./**/*"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/**/*",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user