Initial commit

This commit is contained in:
2023-08-31 21:44:37 -05:00
commit f7a8fe6ea8
23 changed files with 10464 additions and 0 deletions

27
.editorconfig Normal file
View 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
View 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
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

64
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,6 @@
package-lock.json
tsconfig.json
node_modules
dist
coverage

25
.prettierrc.json Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
# threetwo-acquisition-service
A ThreeTwo service for interacting with various torrent clients

10
jest.config.js Normal file
View 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
View 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 brokers deeply nested default options, which are not presented in "moleculer.config.js",
* use the `MOL_` prefix and double underscore `__` for nested properties in .env file.
* For example, to set the cacher prefix to `MYCACHE`, you should declare an env var as `MOL_CACHER__OPTIONS__PREFIX=mycache`.
* It will set this:
* {
* cacher: {
* options: {
* prefix: "mycache"
* }
* }
* }
*/
const brokerConfig: BrokerOptions = {
// Namespace of nodes to segment your nodes on the same network.
namespace: "",
// 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

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

726
public/index.html Normal file
View 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 || "&lt;not set&gt;" }}</div>
</div>
<div class="box">
<div class="caption">Transporter</div>
<div class="value">{{ broker.transporter || "&lt;no transporter&gt;" }}</div>
</div>
<div class="box">
<div class="caption">Serializer</div>
<div class="value">{{ broker.serializer || "JSON" }}</div>
</div>
<div class="box">
<div class="caption">Strategy</div>
<div class="value">{{ broker.registry.strategy || "Round Robin" }}</div>
</div>
<div class="box">
<div class="caption">Cacher</div>
<div class="value">{{ broker.cacher ? "Enabled" : "Disabled" }}</div>
</div>
<div class="box">
<div class="caption">Logger</div>
<div class="value">{{ broker.logger ? "Enabled" : "Disabled" }}</div>
</div>
<div class="box">
<div class="caption">Metrics</div>
<div class="value">{{ broker.metrics.enabled ? "Enabled" : "Disabled" }}</div>
</div>
<div class="box">
<div class="caption">Tracing</div>
<div class="value">{{ broker.tracing.enabled ? "Enabled" : "Disabled" }}</div>
</div>
</div>
<h3 class="cursor-pointer" @click="showBrokerOptions = !showBrokerOptions">Broker options <i :class="'fa fa-angle-' + (showBrokerOptions ? 'up' : 'down')"></i></h3>
<pre v-if="showBrokerOptions" class="broker-options"><code>{{ broker }}</code></pre>
</template>
</div>
</section>
<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 &copy; 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
View 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
View 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;

View 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: {},
});
}
}

View 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");
});
});
});

View 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
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["**/*.spec.ts"]
}

12
tsconfig.eslint.json Normal file
View 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
View 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. */
}
}