Initial commit
This commit is contained in:
27
.editorconfig
Normal file
27
.editorconfig
Normal file
@@ -0,0 +1,27 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
trim_trailing_whitespace = false
|
||||
indent_size = 2
|
||||
|
||||
[*.{cjs,mjs,js,ts,tsx,css,html}]
|
||||
indent_style = tab
|
||||
indent_size = unset # reset to ide view preferences
|
||||
270
.eslintrc.js
Normal file
270
.eslintrc.js
Normal file
@@ -0,0 +1,270 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"airbnb-base",
|
||||
"airbnb-typescript/base",
|
||||
"plugin:jest/recommended",
|
||||
"plugin:jest/style",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
"prettier",
|
||||
],
|
||||
|
||||
parserOptions: { tsconfigRootDir: __dirname, project: "./tsconfig.eslint.json" },
|
||||
|
||||
env: { es2021: true, node: true, "jest/globals": true },
|
||||
|
||||
plugins: ["jest"],
|
||||
|
||||
ignorePatterns: [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"coverage",
|
||||
// "**/*.d.ts",
|
||||
"!.*.js",
|
||||
"!.*.cjs",
|
||||
"!.*.mjs",
|
||||
],
|
||||
|
||||
rules: {
|
||||
// enforce curly brace usage
|
||||
curly: ["error", "all"],
|
||||
|
||||
// allow class methods which do not use this
|
||||
"class-methods-use-this": "off",
|
||||
|
||||
// Allow use of ForOfStatement - no-restricted-syntax does not allow us to turn off a rule. This block overrides the airbnb rule entirely
|
||||
// https://github.com/airbnb/javascript/blob/7152396219e290426a03e47837e53af6bcd36bbe/packages/eslint-config-airbnb-base/rules/style.js#L257-L263
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
selector: "ForInStatement",
|
||||
message:
|
||||
"for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.",
|
||||
},
|
||||
{
|
||||
selector: "LabeledStatement",
|
||||
message:
|
||||
"Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.",
|
||||
},
|
||||
{
|
||||
selector: "WithStatement",
|
||||
message:
|
||||
"`with` is disallowed in strict mode because it makes code impossible to predict and optimize.",
|
||||
},
|
||||
],
|
||||
|
||||
// underscore dangle will be handled by @typescript-eslint/naming-convention
|
||||
"no-underscore-dangle": "off",
|
||||
|
||||
// enforce consistent sort order
|
||||
"sort-imports": ["error", { ignoreCase: true, ignoreDeclarationSort: true }],
|
||||
|
||||
// enforce convention in import order
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "never",
|
||||
groups: ["builtin", "external", "internal", "parent", "sibling", "index"],
|
||||
alphabetize: { order: "asc", caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
|
||||
// ensure consistent array typings
|
||||
"@typescript-eslint/array-type": "error",
|
||||
|
||||
// ban ts-comment except with description
|
||||
"@typescript-eslint/ban-ts-comment": [
|
||||
"error",
|
||||
{
|
||||
"ts-expect-error": "allow-with-description",
|
||||
"ts-ignore": "allow-with-description",
|
||||
"ts-nocheck": true,
|
||||
"ts-check": false,
|
||||
},
|
||||
],
|
||||
|
||||
// prefer type imports and exports
|
||||
"@typescript-eslint/consistent-type-exports": [
|
||||
"error",
|
||||
{ fixMixedExportsWithInlineTypeSpecifier: true },
|
||||
],
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||
|
||||
// enforce consistent order of class members
|
||||
"@typescript-eslint/member-ordering": "error",
|
||||
|
||||
// set up naming convention rules
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
// camelCase for everything not otherwise indicated
|
||||
{ selector: "default", format: ["camelCase"] },
|
||||
// allow known default exclusions
|
||||
{
|
||||
selector: "default",
|
||||
filter: { regex: "^(_id|__v)$", match: true },
|
||||
format: null,
|
||||
},
|
||||
// allow variables to be camelCase or UPPER_CASE
|
||||
{ selector: "variable", format: ["camelCase", "UPPER_CASE"] },
|
||||
// allow known variable exclusions
|
||||
{
|
||||
selector: "variable",
|
||||
filter: { regex: "^(_id|__v)$", match: true },
|
||||
format: null,
|
||||
},
|
||||
{
|
||||
// variables ending in Service should be PascalCase
|
||||
selector: "variable",
|
||||
filter: { regex: "^.*Service$", match: true },
|
||||
format: ["PascalCase"],
|
||||
},
|
||||
// do not enforce format on property names
|
||||
{ selector: "property", format: null },
|
||||
// PascalCase for classes and TypeScript keywords
|
||||
{
|
||||
selector: ["typeLike"],
|
||||
format: ["PascalCase"],
|
||||
},
|
||||
],
|
||||
|
||||
// disallow parameter properties in favor of explicit class declarations
|
||||
"@typescript-eslint/no-parameter-properties": "error",
|
||||
|
||||
// ensure unused variables are treated as an error
|
||||
// overrides @typescript-eslint/recommended -- '@typescript-eslint/no-unused-vars': 'warn'
|
||||
// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended.ts
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
},
|
||||
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
|
||||
rules: {
|
||||
// disable rules turned on by @typescript-eslint/recommended-requiring-type-checking which are too noisy
|
||||
// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/unbound-method": "off",
|
||||
|
||||
// force explicit member accessibility modifiers
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", { accessibility: "no-public" }],
|
||||
|
||||
// enforce return types on module boundaries
|
||||
"@typescript-eslint/explicit-module-boundary-types": "error",
|
||||
|
||||
// allow empty functions
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ["**/index.ts"],
|
||||
rules: {
|
||||
// prefer named exports for certain file types
|
||||
"import/prefer-default-export": "off",
|
||||
"import/no-default-export": "error",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: [
|
||||
"**/test/**/*.[jt]s?(x)",
|
||||
"**/__tests__/**/*.[jt]s?(x)",
|
||||
"**/?(*.)+(spec|test).[jt]s?(x)",
|
||||
],
|
||||
rules: {
|
||||
// allow tests to create multiple classes
|
||||
"max-classes-per-file": "off",
|
||||
|
||||
// allow side effect constructors
|
||||
"no-new": "off",
|
||||
|
||||
// allow import with CommonJS export
|
||||
"import/no-import-module-exports": "off",
|
||||
|
||||
// allow dev dependencies
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error",
|
||||
{ devDependencies: true, optionalDependencies: false, peerDependencies: false },
|
||||
],
|
||||
|
||||
// disallow use of "it" for test blocks
|
||||
"jest/consistent-test-it": ["error", { fn: "test", withinDescribe: "test" }],
|
||||
|
||||
// ensure all tests contain an assertion
|
||||
"jest/expect-expect": "error",
|
||||
|
||||
// no commented out tests
|
||||
"jest/no-commented-out-tests": "error",
|
||||
|
||||
// no duplicate test hooks
|
||||
"jest/no-duplicate-hooks": "error",
|
||||
|
||||
// valid titles
|
||||
"jest/valid-title": "error",
|
||||
|
||||
// no if conditionals in tests
|
||||
"jest/no-if": "error",
|
||||
|
||||
// expect statements in test blocks
|
||||
"jest/no-standalone-expect": "error",
|
||||
|
||||
// disallow returning from test
|
||||
"jest/no-test-return-statement": "error",
|
||||
|
||||
// disallow truthy and falsy in tests
|
||||
"jest/no-restricted-matchers": ["error", { toBeFalsy: null, toBeTruthy: null }],
|
||||
|
||||
// prefer called with
|
||||
"jest/prefer-called-with": "error",
|
||||
|
||||
"jest/no-conditional-expect": "off"
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: [
|
||||
"**/test/**/*.ts?(x)",
|
||||
"**/__tests__/**/*.ts?(x)",
|
||||
"**/?(*.)+(spec|test).ts?(x)",
|
||||
],
|
||||
rules: {
|
||||
// allow explicit any in tests
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
|
||||
// allow non-null-assertions
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// allow empty arrow functions
|
||||
"@typescript-eslint/no-empty-function": ["warn", { allow: ["arrowFunctions"] }],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ["./.*.js", "./*.js"],
|
||||
rules: {
|
||||
// allow requires in config files
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ["**/*.d.ts"],
|
||||
rules: {
|
||||
// allow tests to create multiple classes
|
||||
"max-classes-per-file": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
|
||||
"lines-between-class-members": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"@typescript-eslint/member-ordering": "off",
|
||||
"@typescript-eslint/ban-types": "off"
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
# 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/
|
||||
|
||||
# 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/
|
||||
6
.prettierignore
Normal file
6
.prettierignore
Normal file
@@ -0,0 +1,6 @@
|
||||
package-lock.json
|
||||
tsconfig.json
|
||||
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
25
.prettierrc.json
Normal file
25
.prettierrc.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"options": { "parser": "vue" }
|
||||
},
|
||||
{
|
||||
"files": ["*.{cjs,mjs,js,jsx,ts,tsx,d.ts,css,html,graphql}"],
|
||||
"options": { "useTabs": true }
|
||||
},
|
||||
{
|
||||
"files": ["*.{json,yml,yaml}"],
|
||||
"options": { "tabWidth": 2 }
|
||||
}
|
||||
]
|
||||
}
|
||||
36
.vscode/launch.json
vendored
Normal file
36
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible Node.js debug attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceRoot}/node_modules/moleculer/bin/moleculer-runner.js",
|
||||
"sourceMaps": true,
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"args": [
|
||||
"services/**/*.service.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Jest",
|
||||
"program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js",
|
||||
"args": [
|
||||
"--runInBand"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeArgs": [
|
||||
"--nolazy"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Rishi Ghan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# threetwo-acquisition-service
|
||||
A ThreeTwo service for interacting with various torrent clients
|
||||
10
jest.config.js
Normal file
10
jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
coverageDirectory: "./coverage",
|
||||
rootDir: "./",
|
||||
roots: [
|
||||
"./test"
|
||||
]
|
||||
};
|
||||
210
moleculer.config.ts
Normal file
210
moleculer.config.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { BrokerOptions, MetricRegistry, ServiceBroker } from "moleculer";
|
||||
import { Errors } 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, // "Redis"
|
||||
|
||||
// Define a cacher.
|
||||
// More info: https://moleculer.services/docs/0.14/caching.html
|
||||
cacher: "Redis",
|
||||
|
||||
// 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: Error) =>
|
||||
err && err instanceof Errors.MoleculerRetryableError && !!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: Error) => err && err instanceof Errors.MoleculerError && err.code >= 500,
|
||||
},
|
||||
|
||||
// Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead
|
||||
bulkhead: {
|
||||
// Enable feature.
|
||||
enabled: false,
|
||||
// Maximum concurrent executions.
|
||||
concurrency: 10,
|
||||
// Maximum size of queue
|
||||
maxQueueSize: 100,
|
||||
},
|
||||
|
||||
// Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html
|
||||
validator: true,
|
||||
|
||||
errorHandler: null,
|
||||
|
||||
// Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html
|
||||
metrics: {
|
||||
enabled: false,
|
||||
// Available built-in reporters: "Console", "CSV", "Event", "Prometheus", "Datadog", "StatsD"
|
||||
reporter: {
|
||||
type: "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.
|
||||
// async started(broker: ServiceBroker): Promise<void> {},
|
||||
|
||||
// Called after broker stopped.
|
||||
// async stopped(broker: ServiceBroker): Promise<void> {},
|
||||
};
|
||||
|
||||
export = brokerConfig;
|
||||
7655
package-lock.json
generated
Normal file
7655
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "threetwo-acquisition-service",
|
||||
"version": "1.0.0",
|
||||
"description": "My Moleculer-based microservices project",
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"dev": "ts-node ./node_modules/moleculer/bin/moleculer-runner.js --config moleculer.config.ts --hot --repl services/**/*.service.ts",
|
||||
"start": "moleculer-runner --config dist/moleculer.config.js",
|
||||
"test:types": "concurrently npm:prettier npm:lint npm:typecheck",
|
||||
"typecheck": "tsc --noEmit && echo \"tsc: no typecheck errors\"",
|
||||
"ci": "jest --watch",
|
||||
"test": "jest --coverage",
|
||||
"lint": "cross-env TIMING=1 eslint . --ext cjs,mjs,js,jsx,ts,tsx",
|
||||
"lint:fix": "cross-env TIMING=1 eslint . --ext cjs,mjs,js,jsx,ts,tsx --fix",
|
||||
"prettier": "prettier . --ignore-unknown --check",
|
||||
"prettier:fix": "prettier . --ignore-unknown --write"
|
||||
},
|
||||
"keywords": [
|
||||
"microservices",
|
||||
"moleculer"
|
||||
],
|
||||
"author": "",
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.44.0",
|
||||
"@typescript-eslint/parser": "^5.44.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jest": "^27.1.6",
|
||||
"prettier": "^2.8.0",
|
||||
"@jest/globals": "^29.3.1",
|
||||
"@types/jest": "^29.2.3",
|
||||
"@types/node": "^18.11.9",
|
||||
"concurrently": "^7.6.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jest": "^29.3.1",
|
||||
"moleculer-repl": "^0.7.3",
|
||||
"ts-jest": "^29.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"moleculer-web": "^0.10.5",
|
||||
"ioredis": "^5.0.0",
|
||||
"moleculer": "^0.14.27"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.x.x"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
726
public/index.html
Normal file
726
public/index.html
Normal file
@@ -0,0 +1,726 @@
|
||||
|
||||
<!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>threetwo-acquisition-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@3.2.34/dist/vue.global.js"></script>
|
||||
<link rel="stylesheet" href="./main.css" />
|
||||
</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>
|
||||
<section id="apis" v-if="page == 'apis'">
|
||||
<div class="flex row no-wrap m-y-sm ">
|
||||
<input type="text" class="input-size-md flex-grow" placeholder="Search in actions, methods, paths..." v-model="apiSearchText" />
|
||||
<button class="m-x-xs button outlined positive" @click="refreshApiPage">
|
||||
<i class="fa fa-refresh"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<button :class="`button ${globalAuth?.token ? 'positive' : 'outlined negative'}`" @click="showAuthorizeDialog">
|
||||
<i :class="`fa fa-${globalAuth?.token ? 'lock' : 'unlock'}`"></i>
|
||||
Authorize
|
||||
</button>
|
||||
</div>
|
||||
<hr/>
|
||||
<template v-for="(section, name) in filteredApis" :key="name">
|
||||
<section v-if="section && section.length>0" :id="name">
|
||||
<fieldset>
|
||||
<legend>
|
||||
{{ getService(name).fullName }}<span v-if="getService(name).version" class="badge light m-x-xs">{{ getService(name).version }}</span>
|
||||
</legend>
|
||||
<div class="content">
|
||||
<div :class="`action-card action-method-${item.rest.method.toLocaleLowerCase()} `" v-for="item,ix in section" :key="ix" >
|
||||
<div class="action-card-header" @click="item.expand=!item.expand">
|
||||
<span :class="`badge lg fixed text-center text-code bg-method-${item.rest.method.toLocaleLowerCase()} `"> {{ item.rest.method }}</span>
|
||||
<span class="text-subtitle2 m-x-xs">{{ item.rest.path }}</span>
|
||||
<div class="flex-spacer"></div>
|
||||
<span class="text-caption m-x-xs">{{ item.action }}</span>
|
||||
<span class="badge m-x-xs">{{ item.fields.length }}</span>
|
||||
</div>
|
||||
<form @submit.prevent.stop="callAction(item,name)">
|
||||
<div :class="{'action-card-section':true,expand:item.expand}">
|
||||
<div class="action-card-section-parameters">
|
||||
<div class="action-card-section-parameters-header">
|
||||
|
||||
<div class="text-p">Parameters</div>
|
||||
<div class="flex-spacer"></div>
|
||||
<div class="">
|
||||
<button :disabled="item.loading" class="button" type="submit">
|
||||
<i :class="`fa fa-${item.loading ? 'spinner':'rocket'}`"></i>
|
||||
{{item.loading ? 'Trying...' : 'Try'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-card-section-parameters-body">
|
||||
<div v-if="item.fields" class="parameters">
|
||||
<div :class="{field:true,required:field.optional===false}" v-for="field,ix in item.fields" :key="field.name">
|
||||
<label :for="field.name+'--'+ix">{{ field.label }}: </label>
|
||||
<input v-if="field.dataType==='number'" :min="field.min" :max="field.max" :type="field.type" :id="field.name+'--'+ix" :name="field.name" v-model.number="field.value" :required="field.required === true || field.optional===false" />
|
||||
<input v-else :type="field.type" :maxlength="field.maxLength" :minlength="field.minLength" :id="field.name+'--'+ix" :name="field.name" v-model="field.value" :required="field.required === true || field.optional===false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-card-section-response" v-if="item.status">
|
||||
<div class="action-card-section-response-header">
|
||||
<div class="text-p">Response</div>
|
||||
<span text>
|
||||
<div class="badge m-x-xs" :class="{ green: item.status < 400, red: item.status >= 400 || item.status == 'ERR' }">{{ item.status }}</div>
|
||||
<div class="badge time m-r-xs">{{ humanize(item.duration) }}</div>
|
||||
</span>
|
||||
<div class="flex-spacer"></div>
|
||||
<div>
|
||||
<button v-if="item.response" class="button outlined negative" @click="clearResponse(item)">
|
||||
<i :class="`fa fa-remove`"></i>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-card-section-response-body">
|
||||
<pre><code>{{ item.response }}</code></pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
<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-2022 - 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 v-if="openAuthorizeDialog" >
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="text-title text-bold">Authorization</span>
|
||||
<span class="modal-close" @click="openAuthorizeDialog = false"></span>
|
||||
</div>
|
||||
<div class="modal-content">
|
||||
<fieldset>
|
||||
<legend>Authorize by username and password</legend>
|
||||
<div class="flex column">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" v-model="auth.username" class="form-control" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input type="password" v-model="auth.password" class="form-control" placeholder="Password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tenant</label>
|
||||
<input type="text" v-model="auth.tenant" class="form-control" placeholder="Tenant">
|
||||
</div>
|
||||
<button class="self-end button outlined positive" @click="authorize">Authorize</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Token</label>
|
||||
<textarea style="height:100px;width: 100%;" v-model="auth.token" class="form-control" placeholder="Token" ></textarea>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
|
||||
<button class="button flat" @click="openAuthorizeDialog = false">Cancel</button>
|
||||
<button class="button flat m-x-xs" @click="resetAuthorization">Reset</button>
|
||||
<button class="button" @click="saveAuthorize">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
const { createApp } = Vue
|
||||
const app = createApp({
|
||||
|
||||
data() {
|
||||
return {
|
||||
apiSearchText: "",
|
||||
menu: [
|
||||
{ id: "home", caption: "Home" },
|
||||
{ id: "apis", caption: "REST API" },
|
||||
{ id: "nodes", caption: "Nodes" },
|
||||
{ id: "services", caption: "Services" }
|
||||
],
|
||||
page: "home",
|
||||
|
||||
requests: {
|
||||
|
||||
},
|
||||
openAuthorizeDialog: false,
|
||||
auth: {
|
||||
tenant:"",
|
||||
username: "",
|
||||
password: "",
|
||||
token: ""
|
||||
},
|
||||
globalAuth:{
|
||||
tenant:"",
|
||||
username: "",
|
||||
password: "",
|
||||
token: ""
|
||||
},
|
||||
fields: {
|
||||
|
||||
},
|
||||
|
||||
broker: null,
|
||||
nodes: [],
|
||||
services: [],
|
||||
actions: {},
|
||||
|
||||
showBrokerOptions: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredServices() {
|
||||
return this.services.filter(svc => !svc.name.startsWith("$"));
|
||||
},
|
||||
|
||||
filteredApis() {
|
||||
const s = this.apiSearchText.toLocaleLowerCase();
|
||||
if (!this.apiSearchText)
|
||||
return this.requests;
|
||||
else {
|
||||
const reqs = {};
|
||||
for (const key in this.requests) {
|
||||
reqs[key] = this.requests[key]
|
||||
.filter(r => r?.action?.toLocaleLowerCase().includes(s) ||
|
||||
r?.rest?.method?.toLocaleLowerCase().includes(s) ||
|
||||
r?.rest?.path?.toLocaleLowerCase().includes(s) ||
|
||||
r?.rest?.url?.toLocaleLowerCase().includes(s));
|
||||
}
|
||||
return reqs;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetAuthorization() {
|
||||
this.auth = {
|
||||
tenant:"",
|
||||
username: "",
|
||||
password: "",
|
||||
token: ""
|
||||
};
|
||||
this.saveAuthorize();
|
||||
},
|
||||
|
||||
authorize() {
|
||||
fetch("/api/v1/identity/auth/signin",{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(this.auth)
|
||||
|
||||
}).then(res => {
|
||||
if (res.status == 401) {
|
||||
this.openAuthorizeDialog = true;
|
||||
alert("Invalid username or password");
|
||||
} else if (res.status == 200) {
|
||||
res.json().then(data => {
|
||||
this.auth.token = res.headers.get("Authorization") || data.token;
|
||||
this.auth.tenant = res.headers.get("x-tenant-id") || data.tenant;
|
||||
// this.saveAuthorize();
|
||||
});
|
||||
}
|
||||
else {
|
||||
alert("Not authorized");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
saveAuthorize() {
|
||||
this.globalAuth = {...this.auth};
|
||||
localStorage.setItem("globalAuth", JSON.stringify(this.globalAuth));
|
||||
this.openAuthorizeDialog = false;
|
||||
},
|
||||
|
||||
refreshApiPage(){
|
||||
return this.updateServiceList();
|
||||
},
|
||||
|
||||
showAuthorizeDialog() {
|
||||
this.openAuthorizeDialog = true;
|
||||
},
|
||||
|
||||
closeAuthorizeDialog(){
|
||||
this.openAuthorizeDialog = false;
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
this.page = page;
|
||||
localStorage.setItem("lastPage", this.page);
|
||||
if (this.page == 'apis') {
|
||||
return this.updateServiceList();
|
||||
}
|
||||
else {
|
||||
this.updatePageResources();
|
||||
}
|
||||
},
|
||||
|
||||
humanize(ms) {
|
||||
return ms > 1500 ? (ms / 1500).toFixed(2) + " s" : ms + " ms";
|
||||
},
|
||||
|
||||
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 "";
|
||||
},
|
||||
|
||||
getRest(item) {
|
||||
if(!item.rest) return item.rest;
|
||||
if (typeof item.rest === "object") return item.rest; // REST object
|
||||
if (item.rest.indexOf(" ") !== -1) {
|
||||
const p = item.rest.split(" ");
|
||||
return { method: p[0], path: p[1] };
|
||||
} else {
|
||||
return { method: "*", path: item.rest };
|
||||
}
|
||||
},
|
||||
|
||||
getFields(item,method,url) {
|
||||
if(!item.params) return [];
|
||||
const r = [];
|
||||
for (const key in item.params) {
|
||||
if(key.startsWith('$')) continue;
|
||||
if(item.params[key].readonly===true) continue;
|
||||
if(item.params[key].hidden===true) continue;
|
||||
const dataType = item.params[key].type || item.params[key];
|
||||
const hidden = item.params[key].hidden || false;
|
||||
const required = item.params[key].required || false;
|
||||
const optional = Array.isArray(item.params[key]) ? item.params[key].every(xx=>xx.optional===true) : item.params[key].optional || false;
|
||||
const maxLength = item.params[key].max || undefined;
|
||||
const minLength = item.params[key].min || undefined;
|
||||
const pattern = item.params[key].pattern || undefined;
|
||||
let type = "text";
|
||||
let value = item.params[key].default || undefined;
|
||||
if (dataType.includes("number")) {type = "number"; };
|
||||
if (dataType === "boolean") {type = "checkbox"; value = value || false;};
|
||||
if (dataType === "string") type = "text";
|
||||
if (dataType === "object") type = "textarea";
|
||||
if (dataType === "array") type = "textarea";
|
||||
if (dataType === "file") type = "file";
|
||||
if (dataType === "date") type = "date";
|
||||
if (dataType === "datetime") type = "datetime";
|
||||
if (dataType === "time") type = "time";
|
||||
if (dataType === "password") type = "password";
|
||||
if (dataType === "enum") type = "select";
|
||||
if (dataType === "enum-multi") type = "select-multi";
|
||||
|
||||
r.push({ name: key,
|
||||
label: key,optional,
|
||||
hidden,required,
|
||||
[type==='number'?'min':'minLength'] :minLength,
|
||||
[type==='number'?'max':'maxLength'] :maxLength,
|
||||
pattern,
|
||||
paramType: method==='GET' ? 'param' : 'body',
|
||||
value,
|
||||
type,dataType, value:undefined });
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
getService(fullName){
|
||||
const svc = this.services.find(svc => svc.fullName == fullName);
|
||||
return svc || {};
|
||||
},
|
||||
|
||||
clearResponse(item){
|
||||
item.response = undefined;
|
||||
item.duration = undefined;
|
||||
item.loading = false;
|
||||
item.status = undefined;
|
||||
},
|
||||
|
||||
callAction: function (item,fullName) {
|
||||
if(!item.rest) return;
|
||||
item.loading = true;
|
||||
const service = this.services.find(svc => svc.name == fullName);
|
||||
var startTime = Date.now();
|
||||
|
||||
const method = item.rest.method || "GET";
|
||||
let url = item.rest.url;
|
||||
let fields = item.fields;
|
||||
let body = null;
|
||||
let params = null;
|
||||
if (fields) {
|
||||
body = {};
|
||||
params = {};
|
||||
fields.forEach(field => {
|
||||
const value = field.value;
|
||||
if (field.paramType == "body"){
|
||||
body[field.name] = value;
|
||||
if(value===undefined && field.optional===true){
|
||||
delete body[field.name];
|
||||
}
|
||||
}
|
||||
else if (field.paramType == "param"){
|
||||
|
||||
params[field.name] = value;
|
||||
if(value===undefined && field.optional===true){
|
||||
delete params[field.name];
|
||||
}
|
||||
}
|
||||
|
||||
else if (field.paramType == "url"){
|
||||
if(value===undefined && field.optional===true){
|
||||
url = url.replace(`:${field.name}`,'');
|
||||
}
|
||||
else{
|
||||
url = url.replace(`:${field.name}`,value);
|
||||
}
|
||||
}
|
||||
url = url.replace(`:${field.name}`,value);
|
||||
});
|
||||
|
||||
if (body && method == "GET") {
|
||||
body = null;
|
||||
}
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const qparams = {};
|
||||
for (const key in params) {
|
||||
if(params[key]!==undefined){
|
||||
qparams[key] = params[key];
|
||||
}
|
||||
}
|
||||
url += "?" + new URLSearchParams(qparams).toString();
|
||||
}
|
||||
|
||||
}
|
||||
const authtoken = this.globalAuth.token;
|
||||
const tenant = this.globalAuth.tenant;
|
||||
const authHeader = {};
|
||||
if(authtoken){
|
||||
authHeader['Authorization'] = `Bearer ${authtoken}`;
|
||||
}
|
||||
if(tenant){
|
||||
authHeader["x-tenant"] = tenant;
|
||||
}
|
||||
return fetch(url, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : null,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authHeader
|
||||
}
|
||||
}).then(function(res) {
|
||||
item.status = res.status;
|
||||
item.duration = Date.now() - startTime;
|
||||
return res.json().then(json => {
|
||||
item.response = json;
|
||||
item.loading = false;
|
||||
if (item.afterResponse)
|
||||
return item.afterResponse(json);
|
||||
});
|
||||
}).catch(function (err) {
|
||||
item.status = "ERR";
|
||||
item.duration = Date.now() - startTime;
|
||||
item.response = err.message;
|
||||
item.loading = false;
|
||||
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;
|
||||
}, {});
|
||||
this.actions = actions;
|
||||
if(this.page==='apis'){
|
||||
this.requests = {};
|
||||
for (const service of this.services) {
|
||||
this.requests[service.fullName] = [];
|
||||
const version = service.version ? "v"+service.version+"/" : "";
|
||||
for (const key in service.actions) {
|
||||
const action = service.actions[key];
|
||||
if(!action.rest) continue;
|
||||
const req = {
|
||||
expand:false,
|
||||
loading:false,
|
||||
id: action.name,
|
||||
action: action.name,
|
||||
rest: this.getRest(action),
|
||||
fields: action.fields,
|
||||
response: null,
|
||||
status: null,
|
||||
duration: null,
|
||||
afterResponse: action.afterResponse
|
||||
};
|
||||
const baseUrl = service.settings.rest;
|
||||
if(req.rest.method==='*'){
|
||||
['GET','POST','PUT','PATCH','DELETE'].forEach(method => {
|
||||
const req2 = Object.assign({}, req);
|
||||
req2.id = req2.id+'.'+method.toLocaleLowerCase();
|
||||
req2.rest = Object.assign({}, req.rest);
|
||||
req2.rest.method = method;
|
||||
const url = baseUrl ? `/api${baseUrl}${req2.rest.path}` : `/api/${version}${service.name}${req2.rest.path}`;
|
||||
req2.rest.url = url;
|
||||
req2.fields = this.getFields(action,req2.rest.method,req2.rest.url);
|
||||
this.requests[service.fullName].push(req2);
|
||||
});
|
||||
} else {
|
||||
let version = service.version ? "v"+service.version+"/" : "";
|
||||
let url = baseUrl ? `/api${baseUrl}${req.rest.path}`: `/api/${version}${service.name}${req.rest.path}`;
|
||||
req.rest.url = url;
|
||||
req.fields = this.getFields(action,req.rest.method,req.rest.url);
|
||||
this.requests[service.fullName].push(req);
|
||||
}
|
||||
|
||||
}
|
||||
if(this.requests[service.fullName].length===0) delete this.requests[service.fullName];
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
const page = localStorage.getItem("lastPage");
|
||||
this.page = page ? page : 'home';
|
||||
if(this.page==='apis'){
|
||||
this.refreshApiPage();
|
||||
}
|
||||
const globalAuth = localStorage.getItem("globalAuth");
|
||||
this.globalAuth = globalAuth ? JSON.parse(globalAuth) : {};
|
||||
|
||||
setInterval(function () {
|
||||
self.updatePageResources();
|
||||
}, 2000);
|
||||
|
||||
this.updateBrokerOptions();
|
||||
}
|
||||
|
||||
});
|
||||
app.mount('#app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
931
public/main.css
Normal file
931
public/main.css
Normal file
@@ -0,0 +1,931 @@
|
||||
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;
|
||||
}
|
||||
|
||||
.m-r-xs{
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.m-l-xs{
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.m-t-xs{
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.m-b-xs{
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.m-x-xs{
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.m-y-xs{
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.m-t-sm{
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.m-b-sm{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.m-x-sm{
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.m-y-sm{
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.m-t-md{
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.m-b-md{
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.m-x-md{
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
}
|
||||
|
||||
.m-y-md{
|
||||
margin-top: 2em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.m-t-lg{
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.m-b-lg{
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
.m-x-lg{
|
||||
margin-left: 3em;
|
||||
margin-right: 3em;
|
||||
}
|
||||
|
||||
.m-y-lg{
|
||||
margin-top: 3em;
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
|
||||
.m-t-xl{
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
min-width: 100px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button i, .button i {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
button:hover, .button:hover {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.button.outlined{
|
||||
background-color: transparent;
|
||||
border: 1px solid #3CAFCE;
|
||||
color: #3CAFCE;
|
||||
}
|
||||
|
||||
.button.flat{
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
color: #3CAFCE;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.button.flat:hover{
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,.2);
|
||||
transition: .1s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.button.flat.negative{
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
color: #b2184e;
|
||||
}
|
||||
.button.flat.positive{
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
color: #28a728;
|
||||
}
|
||||
.button.flat.info{
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
color: #285fa7;
|
||||
}
|
||||
.button.flat.warning{
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
color: #b2ad18;
|
||||
}
|
||||
|
||||
.button.outlined.negative{
|
||||
background-color: transparent;
|
||||
border: 1px solid #b2184e;
|
||||
color: #b2184e;
|
||||
}
|
||||
.button.outlined.positive{
|
||||
background-color: transparent;
|
||||
border: 1px solid #28a728;
|
||||
color: #28a728;
|
||||
}
|
||||
.button.outlined.info{
|
||||
background-color: transparent;
|
||||
border: 1px solid #285fa7;
|
||||
color: #285fa7;
|
||||
}
|
||||
.button.outlined.warning{
|
||||
background-color: transparent;
|
||||
border: 1px solid #b2ad18;
|
||||
color: #b2ad18;
|
||||
}
|
||||
|
||||
|
||||
.button.negative{
|
||||
background-color: #b2184e;
|
||||
}
|
||||
.button.positive{
|
||||
background-color: #28a728;
|
||||
}
|
||||
.button.info{
|
||||
background-color: #285fa7;
|
||||
}
|
||||
.button.warning{
|
||||
background-color: #b2ad18;
|
||||
}
|
||||
|
||||
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: rgba(240, 244, 247, 0.802);
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
main fieldset legend {
|
||||
background-color: #cce7ff;
|
||||
border: 1px solid lightgrey;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
main fieldset .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
main fieldset .action-card {
|
||||
|
||||
}
|
||||
|
||||
.action-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin-bottom: .2em;
|
||||
margin-top: .2em;
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 4px;
|
||||
|
||||
}
|
||||
|
||||
.action-card.expand {
|
||||
|
||||
}
|
||||
|
||||
.action-card-header{
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex:1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
transition: .25s ease-in-out all;
|
||||
}
|
||||
|
||||
.action-card-header:hover{
|
||||
filter: brightness(1.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-card-header.expand{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.action-card-section{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-card-section.expand{
|
||||
display: block;
|
||||
transition: .300s ease-in-out display;
|
||||
}
|
||||
|
||||
.flex-spacer{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
.action-card-section-parameters{
|
||||
|
||||
|
||||
}
|
||||
.action-card-section-parameters-header{
|
||||
background-color: #fbfbfbbb;
|
||||
padding: 8px;
|
||||
display: flex;justify-items: center;align-items: center;flex-direction: row; flex: 1;
|
||||
|
||||
}
|
||||
.action-card-section-parameters-body{
|
||||
padding: 8px;
|
||||
|
||||
}
|
||||
|
||||
.action-card-section-response{
|
||||
background-color: #fbfbfb92;
|
||||
}
|
||||
|
||||
.action-card-section-response-header{
|
||||
background-color: #fbfbfbbb;
|
||||
padding: 8px;
|
||||
display: flex;justify-items: center;align-items: center;flex-direction: row; flex: 1;
|
||||
}
|
||||
|
||||
.action-card-section-response-body{
|
||||
padding: 4px 16px;
|
||||
}
|
||||
|
||||
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: 4px;
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge.lg {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.badge.lg.fixed {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.badge.green {
|
||||
background-color: limegreen;
|
||||
}
|
||||
|
||||
.badge.red {
|
||||
background-color: firebrick;
|
||||
}
|
||||
|
||||
.badge.orange {
|
||||
background-color: #fab000;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.badge.light {
|
||||
background-color: #669aa9a6;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
input[type=text], input[type=password], input[type=number], input[type=email], input[type=url], input[type=tel], input[type=date], input[type=month], input[type=week], input[type=time], input[type=datetime], input[type=datetime-local], input[type=color], textarea, select {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid rgba(42, 51, 150, 0.806);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
margin-right: 0.5em;
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
}
|
||||
|
||||
input[type=radio] {
|
||||
margin-right: 0.5em;
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
}
|
||||
|
||||
input[required]:invalid {
|
||||
background-color: #d0c0c0d0;
|
||||
border: 1px solid rgb(161, 54, 54);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
|
||||
}
|
||||
|
||||
input[required]:after{
|
||||
content: "*";
|
||||
color: red;
|
||||
font-size: 0.8em;
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
background-color: #3CAFCE;
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.bg-method-post {
|
||||
background-color: #1e8847;
|
||||
}
|
||||
|
||||
.bg-method-get {
|
||||
background-color: #1f697e;
|
||||
}
|
||||
|
||||
.bg-method-put {
|
||||
background-color: #b79f27;
|
||||
}
|
||||
|
||||
.bg-method-patch {
|
||||
background-color: #916d18;
|
||||
}
|
||||
|
||||
.bg-method-delete {
|
||||
background-color: #b72727;
|
||||
}
|
||||
|
||||
.bg-method-options {
|
||||
background-color: #80449a;
|
||||
}
|
||||
|
||||
.action-method-post {
|
||||
background-color: #1e884740;
|
||||
border: 1px solid #1e8847;
|
||||
}
|
||||
|
||||
.action-method-get {
|
||||
background-color: #1f697e44;
|
||||
border: 1px solid #1f697e;
|
||||
}
|
||||
|
||||
.action-method-put {
|
||||
background-color: #b79f2740;
|
||||
border: 1px solid #b79f27;
|
||||
}
|
||||
|
||||
.action-method-patch {
|
||||
background-color: #916d183e;
|
||||
border: 1px solid #916d18;
|
||||
}
|
||||
|
||||
.action-method-delete {
|
||||
background-color: #b727273d;
|
||||
border: 1px solid #b72727;
|
||||
}
|
||||
|
||||
.action-method-options {
|
||||
background-color: #80449a61;
|
||||
border: 1px solid #80449a;
|
||||
}
|
||||
|
||||
|
||||
.text-title {
|
||||
font-size: 1.25em;
|
||||
font-weight: 400;
|
||||
}
|
||||
.text-subtitle1 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.text-subtitle2 {
|
||||
font-size: 1.15em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.text-h1 {
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.text-h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.text-h3 {
|
||||
font-size: 1.25em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-h4 {
|
||||
font-size: 1.15em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-h5 {
|
||||
font-size: 1em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.text-h6 {
|
||||
font-size: 0.85em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.text-caption {
|
||||
font-size: 0.85em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.text-code {
|
||||
font-size: 1em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.text-p {
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 0.85em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 0.85em;
|
||||
font-weight: 400;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #3CAFCE;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-break {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.text-lowercase {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text-capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.text-wrap {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.full-width{
|
||||
width: 100%;
|
||||
}
|
||||
.flex,.row,.column{
|
||||
display: flex;
|
||||
}
|
||||
.column{
|
||||
flex-direction: column;
|
||||
}
|
||||
.row{
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.self-start{
|
||||
align-self: flex-start;
|
||||
}
|
||||
.self-center{
|
||||
align-self: center;
|
||||
}
|
||||
.self-end{
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.justify-start{
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.justify-center{
|
||||
justify-content: center;
|
||||
}
|
||||
.justify-end{
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.justify-between{
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-around{
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.items-start{
|
||||
align-items: flex-start;
|
||||
}
|
||||
.items-center{
|
||||
align-items: center;
|
||||
}
|
||||
.items-end{
|
||||
align-items: flex-end;
|
||||
}
|
||||
.items-baseline{
|
||||
align-items: baseline;
|
||||
}
|
||||
.items-stretch{
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.flex-grow{
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flex-wrap{
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.nowrap{
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal{
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 500px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
|
||||
}
|
||||
|
||||
.modal .modal-header{
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
padding: 8px;
|
||||
}
|
||||
.modal .modal-content{
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
|
||||
}
|
||||
.modal .modal-actions{
|
||||
border-top: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
}
|
||||
.form-group {
|
||||
|
||||
}
|
||||
.form-group > * {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
input[type=text].input-size-md{
|
||||
height: 1.5em;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.field>label {
|
||||
width: 120px;
|
||||
}
|
||||
168
services/api.service.ts
Normal file
168
services/api.service.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import type { Context, ServiceSchema } from "moleculer";
|
||||
import type { ApiSettingsSchema, GatewayResponse, IncomingRequest, Route } from "moleculer-web";
|
||||
import ApiGateway from "moleculer-web";
|
||||
|
||||
interface Meta {
|
||||
userAgent?: string | null | undefined;
|
||||
user?: object | null | undefined;
|
||||
}
|
||||
|
||||
const ApiService: ServiceSchema<ApiSettingsSchema> = {
|
||||
name: "api",
|
||||
mixins: [ApiGateway],
|
||||
|
||||
// More info about settings: https://moleculer.services/docs/0.14/moleculer-web.html
|
||||
settings: {
|
||||
// Exposed port
|
||||
port: process.env.PORT != null ? Number(process.env.PORT) : 3000,
|
||||
|
||||
// Exposed IP
|
||||
ip: "0.0.0.0",
|
||||
|
||||
// Global Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares
|
||||
use: [],
|
||||
|
||||
routes: [
|
||||
{
|
||||
path: "/api",
|
||||
|
||||
whitelist: ["**"],
|
||||
|
||||
// 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.
|
||||
*
|
||||
onBeforeCall(
|
||||
ctx: Context<unknown, Meta>,
|
||||
route: Route,
|
||||
req: IncomingRequest,
|
||||
res: GatewayResponse,
|
||||
): void {
|
||||
// Set request headers to context meta
|
||||
ctx.meta.userAgent = req.headers["user-agent"];
|
||||
}, */
|
||||
|
||||
/**
|
||||
* After call hook. You can modify the data.
|
||||
*
|
||||
onAfterCall(
|
||||
ctx: Context,
|
||||
route: Route,
|
||||
req: IncomingRequest,
|
||||
res: GatewayResponse,
|
||||
data: unknown,
|
||||
): unknown {
|
||||
// Async function which return with Promise
|
||||
// return this.doSomething(ctx, res, data);
|
||||
return 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. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Serve-static-files
|
||||
assets: {
|
||||
folder: "public",
|
||||
|
||||
// Options to `server-static` module
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Authenticate the request. It check the `Authorization` token value in the request header.
|
||||
* Check the token value & resolve the user by the token.
|
||||
* The resolved user will be available in `ctx.meta.user`
|
||||
*
|
||||
* PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION!
|
||||
*/
|
||||
authenticate(
|
||||
ctx: Context,
|
||||
route: Route,
|
||||
req: IncomingRequest,
|
||||
): Record<string, unknown> | null {
|
||||
// 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" };
|
||||
}
|
||||
// Invalid token
|
||||
throw new ApiGateway.Errors.UnAuthorizedError(
|
||||
ApiGateway.Errors.ERR_INVALID_TOKEN,
|
||||
null,
|
||||
);
|
||||
} 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!
|
||||
*/
|
||||
authorize(ctx: Context<null, Meta>, route: Route, req: IncomingRequest) {
|
||||
// Get the authenticated user.
|
||||
const { user } = ctx.meta;
|
||||
|
||||
// It check the `auth` property in action schema.
|
||||
if (req.$action.auth === "required" && !user) {
|
||||
throw new ApiGateway.Errors.UnAuthorizedError("NO_RIGHTS", null);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default ApiService;
|
||||
19
services/greeter.service.ts
Normal file
19
services/greeter.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
"use strict";
|
||||
import { Context, Service, ServiceBroker, ServiceSchema, Errors } from "moleculer";
|
||||
|
||||
export default class TorrentService extends Service {
|
||||
// @ts-ignore
|
||||
public constructor(
|
||||
public broker: ServiceBroker,
|
||||
schema: ServiceSchema<{}> = { name: "torrent" },
|
||||
) {
|
||||
super(broker);
|
||||
this.parseServiceSchema({
|
||||
name: "torrent",
|
||||
mixins: [],
|
||||
hooks: {},
|
||||
actions: {},
|
||||
methods: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
93
test/unit/mixins/db.mixin.spec.ts
Normal file
93
test/unit/mixins/db.mixin.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { Context, ServiceBroker } from "moleculer";
|
||||
import type { ServiceEventHandler, StartedStoppedHandler } 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());
|
||||
|
||||
test("check schema properties", () => {
|
||||
const schema = DbMixin("my-collection");
|
||||
|
||||
expect(schema.mixins).toEqual([DbService]);
|
||||
expect(schema.adapter).toBeInstanceOf(DbService.MemoryAdapter);
|
||||
expect(schema.started).toBeDefined();
|
||||
expect(schema.events!["cache.clean.my-collection"]).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
test("check cache event handler", async () => {
|
||||
jest.spyOn(broker.cacher!, "clean");
|
||||
|
||||
const schema = DbMixin("my-collection");
|
||||
|
||||
await (schema.events!["cache.clean.my-collection"] as ServiceEventHandler).call(
|
||||
{
|
||||
broker,
|
||||
fullName: "my-service",
|
||||
},
|
||||
Context.create(broker),
|
||||
);
|
||||
|
||||
expect(broker.cacher!.clean).toHaveBeenCalledTimes(1);
|
||||
expect(broker.cacher!.clean).toHaveBeenCalledWith("my-service.*");
|
||||
});
|
||||
|
||||
describe("Check service started handler", () => {
|
||||
test("should not call seedDB method", async () => {
|
||||
const schema = DbMixin("my-collection");
|
||||
|
||||
schema.adapter!.count = jest.fn(() => Promise.resolve(10));
|
||||
const seedDBFn = jest.fn();
|
||||
|
||||
await (schema.started as StartedStoppedHandler).call({
|
||||
broker,
|
||||
logger: broker.logger,
|
||||
adapter: schema.adapter,
|
||||
seedDB: seedDBFn,
|
||||
});
|
||||
|
||||
expect(schema.adapter!.count).toHaveBeenCalledTimes(1);
|
||||
expect(schema.adapter!.count).toHaveBeenCalledWith();
|
||||
|
||||
expect(seedDBFn).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("should call seedDB method", async () => {
|
||||
const schema = DbMixin("my-collection");
|
||||
|
||||
schema.adapter!.count = jest.fn(() => Promise.resolve(0));
|
||||
const seedDBFn = jest.fn();
|
||||
|
||||
await (schema.started as StartedStoppedHandler).call({
|
||||
broker,
|
||||
logger: broker.logger,
|
||||
adapter: schema.adapter,
|
||||
seedDB: seedDBFn,
|
||||
});
|
||||
|
||||
expect(schema.adapter!.count).toHaveBeenCalledTimes(2);
|
||||
expect(schema.adapter!.count).toHaveBeenCalledWith();
|
||||
|
||||
expect(seedDBFn).toHaveBeenCalledTimes(1);
|
||||
expect(seedDBFn).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
test("should broadcast a cache clear event", async () => {
|
||||
const schema = DbMixin("my-collection");
|
||||
|
||||
const ctx = Context.create(broker);
|
||||
|
||||
jest.spyOn(ctx, "broadcast");
|
||||
|
||||
await schema.methods!.entityChanged!("update", null, ctx);
|
||||
|
||||
expect(ctx.broadcast).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.broadcast).toHaveBeenCalledWith("cache.clean.my-collection");
|
||||
});
|
||||
});
|
||||
});
|
||||
28
test/unit/services/greeter.spec.ts
Normal file
28
test/unit/services/greeter.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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", () => {
|
||||
test("should return with 'Hello Moleculer'", async () => {
|
||||
const res = await broker.call("greeter.hello");
|
||||
expect(res).toBe("Hello Moleculer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test 'greeter.welcome' action", () => {
|
||||
test("should return with 'Welcome'", async () => {
|
||||
const res = await broker.call("greeter.welcome", { name: "Adam" });
|
||||
expect(res).toBe("Welcome, Adam");
|
||||
});
|
||||
|
||||
test("should reject an ValidationError", async () => {
|
||||
await expect(broker.call("greeter.welcome")).rejects.toThrow(Errors.ValidationError);
|
||||
});
|
||||
});
|
||||
});
|
||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
}
|
||||
12
tsconfig.eslint.json
Normal file
12
tsconfig.eslint.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"./.*.cjs", // root commonjs files
|
||||
"./.*.js", // root javascript config files
|
||||
"**/*.js", // javascript files
|
||||
"**/*.ts" // typescript files
|
||||
]
|
||||
}
|
||||
103
tsconfig.json
Normal file
103
tsconfig.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
"useUnknownInCatchVariables": false, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user