🥇 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