🍔 Added files
This commit is contained in:
35
.editorconfig
Normal file
35
.editorconfig
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
# Change these settings to your own preference
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
space_after_anon_function = true
|
||||||
|
|
||||||
|
# We recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[{package,bower}.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
quote_type = "double"
|
||||||
143
.eslintrc.js
Normal file
143
.eslintrc.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
node: true
|
||||||
|
},
|
||||||
|
ignorePatterns: [ "test/*"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
project: "tsconfig.json",
|
||||||
|
sourceType: "module"
|
||||||
|
},
|
||||||
|
plugins: ["prefer-arrow", "import", "@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/adjacent-overload-signatures": "error",
|
||||||
|
"@typescript-eslint/array-type": "error",
|
||||||
|
"@typescript-eslint/ban-types": "error",
|
||||||
|
"@typescript-eslint/class-name-casing": "off",
|
||||||
|
"@typescript-eslint/consistent-type-assertions": "error",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "error",
|
||||||
|
"@typescript-eslint/explicit-member-accessibility": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
accessibility: "explicit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/indent": [
|
||||||
|
"off",
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
FunctionDeclaration: {
|
||||||
|
parameters: "first"
|
||||||
|
},
|
||||||
|
FunctionExpression: {
|
||||||
|
parameters: "first"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/interface-name-prefix": "off",
|
||||||
|
"@typescript-eslint/member-delimiter-style": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
multiline: {
|
||||||
|
delimiter: "semi",
|
||||||
|
requireLast: true
|
||||||
|
},
|
||||||
|
singleline: {
|
||||||
|
delimiter: "semi",
|
||||||
|
requireLast: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/member-ordering": "error",
|
||||||
|
"@typescript-eslint/no-empty-function": "error",
|
||||||
|
"@typescript-eslint/no-empty-interface": "error",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-misused-new": "error",
|
||||||
|
"@typescript-eslint/no-namespace": "error",
|
||||||
|
"@typescript-eslint/no-parameter-properties": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "true",
|
||||||
|
"@typescript-eslint/prefer-for-of": "error",
|
||||||
|
"@typescript-eslint/prefer-function-type": "error",
|
||||||
|
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||||
|
"@typescript-eslint/quotes": [
|
||||||
|
"error",
|
||||||
|
"double",
|
||||||
|
{
|
||||||
|
avoidEscape: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/semi": ["error", "always"],
|
||||||
|
"@typescript-eslint/triple-slash-reference": "error",
|
||||||
|
"@typescript-eslint/type-annotation-spacing": "error",
|
||||||
|
"@typescript-eslint/unified-signatures": "error",
|
||||||
|
"arrow-body-style": "error",
|
||||||
|
"arrow-parens": ["error", "as-needed"],
|
||||||
|
camelcase: "error",
|
||||||
|
"capitalized-comments": "error",
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
complexity: "off",
|
||||||
|
"constructor-super": "error",
|
||||||
|
curly: "error",
|
||||||
|
"dot-notation": "error",
|
||||||
|
"eol-last": "error",
|
||||||
|
eqeqeq: ["error", "smart"],
|
||||||
|
"guard-for-in": "error",
|
||||||
|
"id-blacklist": ["error", "any", "Number", "number", "String", "string", "Boolean", "boolean", "Undefined", "undefined"],
|
||||||
|
"id-match": "error",
|
||||||
|
"import/order": "error",
|
||||||
|
"max-classes-per-file": ["error", 1],
|
||||||
|
"max-len": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignoreUrls": true ,
|
||||||
|
code: 160
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"new-parens": "error",
|
||||||
|
"no-bitwise": "error",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-cond-assign": "error",
|
||||||
|
"no-console": "off",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-fallthrough": "off",
|
||||||
|
"no-invalid-this": "off",
|
||||||
|
"no-multiple-empty-lines": "error",
|
||||||
|
"no-new-wrappers": "error",
|
||||||
|
"no-shadow": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
hoist: "all"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-throw-literal": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"no-undef-init": "error",
|
||||||
|
"no-underscore-dangle": "error",
|
||||||
|
"no-unsafe-finally": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"no-unused-labels": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"object-shorthand": "error",
|
||||||
|
"one-var": ["error", "never"],
|
||||||
|
"prefer-arrow/prefer-arrow-functions": "error",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"quote-props": ["error", "consistent-as-needed"],
|
||||||
|
radix: "error",
|
||||||
|
"space-before-function-paren": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
anonymous: "never",
|
||||||
|
asyncArrow: "always",
|
||||||
|
named: "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spaced-comment": "error",
|
||||||
|
"use-isnan": "error",
|
||||||
|
"valid-typeof": "off",
|
||||||
|
}
|
||||||
|
};
|
||||||
70
.gitignore
vendored
Normal file
70
.gitignore
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# JetBrains IDE
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Don't track transpiled files
|
||||||
|
dist/
|
||||||
|
comics/
|
||||||
|
userdata/
|
||||||
|
.DS_Store
|
||||||
38
.vscode/launch.json
vendored
Normal file
38
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
// 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": [
|
||||||
|
"--nolazy",
|
||||||
|
"-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": [
|
||||||
|
"--inspect-brk",
|
||||||
|
"--nolazy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
40
README.md
Normal file
40
README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[](https://moleculer.services)
|
||||||
|
|
||||||
|
# threetwo-import-service
|
||||||
|
This is a [Moleculer](https://moleculer.services/)-based microservices project. Generated with the [Moleculer CLI](https://moleculer.services/docs/0.14/moleculer-cli.html).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
Start the project with `npm run dev` command.
|
||||||
|
After starting, open the http://localhost:3000/ URL in your browser.
|
||||||
|
On the welcome page you can test the generated services via API Gateway and check the nodes & services.
|
||||||
|
|
||||||
|
In the terminal, try the following commands:
|
||||||
|
- `nodes` - List all connected nodes.
|
||||||
|
- `actions` - List all registered service actions.
|
||||||
|
- `call greeter.hello` - Call the `greeter.hello` action.
|
||||||
|
- `call greeter.welcome --name John` - Call the `greeter.welcome` action with the `name` parameter.
|
||||||
|
- `call products.list` - List the products (call the `products.list` action).
|
||||||
|
|
||||||
|
|
||||||
|
## Services
|
||||||
|
- **api**: API Gateway services
|
||||||
|
- **greeter**: Sample service with `hello` and `welcome` actions.
|
||||||
|
- **products**: Sample DB service. To use with MongoDB, set `MONGO_URI` environment variables and install MongoDB adapter with `npm i moleculer-db-adapter-mongo`.
|
||||||
|
|
||||||
|
## Mixins
|
||||||
|
- **db.mixin**: Database access mixin for services. Based on [moleculer-db](https://github.com/moleculerjs/moleculer-db#readme)
|
||||||
|
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
|
||||||
|
* Moleculer website: https://moleculer.services/
|
||||||
|
* Moleculer Documentation: https://moleculer.services/docs/0.14/
|
||||||
|
|
||||||
|
## NPM scripts
|
||||||
|
|
||||||
|
- `npm run dev`: Start development mode (load all services locally with hot-reload & REPL)
|
||||||
|
- `npm run start`: Start production mode (set `SERVICES` env variable to load certain services)
|
||||||
|
- `npm run cli`: Start a CLI and connect to production. Don't forget to set production namespace with `--ns` argument in script
|
||||||
|
- `npm run lint`: Run ESLint
|
||||||
|
- `npm run ci`: Run continuous test mode with watching
|
||||||
|
- `npm test`: Run tests & generate coverage report
|
||||||
3
data/products.db
Normal file
3
data/products.db
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{"name":"Samsung Galaxy S10 Plus","quantity":10,"price":704,"_id":"MXoybiJcoo4K1Rmm"}
|
||||||
|
{"name":"iPhone 11 Pro","quantity":25,"price":999,"_id":"2icANVEMoCj6nGjG"}
|
||||||
|
{"name":"Huawei P30 Pro","quantity":15,"price":679,"_id":"6KE9pJvNQ83ez7qi"}
|
||||||
58
interfaces/folder.interface.ts
Normal file
58
interfaces/folder.interface.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
export interface IFolderResponse {
|
||||||
|
data: Array<IFolderData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtractionOptions {
|
||||||
|
extractTarget: string;
|
||||||
|
sourceFolder: string;
|
||||||
|
targetExtractionFolder: string;
|
||||||
|
paginationOptions: IPaginationOptions;
|
||||||
|
extractionMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPaginationOptions {
|
||||||
|
pageLimit: number;
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
export interface IComicVineSearchMatch {
|
||||||
|
description: string;
|
||||||
|
id: number;
|
||||||
|
volumes: string;
|
||||||
|
}
|
||||||
|
export interface IFolderData {
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
containedIn: string;
|
||||||
|
isFile: boolean;
|
||||||
|
isLink: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExplodedPathResponse {
|
||||||
|
exploded: Array<string>;
|
||||||
|
fileName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IComicBookCoverMetadata {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
containedIn: string;
|
||||||
|
fileSize: string;
|
||||||
|
imageHash: string;
|
||||||
|
dimensions: {
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtractedComicBookCoverFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
fileSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExtractComicBookCoverErrorResponse {
|
||||||
|
message: string;
|
||||||
|
errorCode: string;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
20
mixins/db.mixin.ts
Normal file
20
mixins/db.mixin.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const mkdir = require("mkdirp").sync;
|
||||||
|
const DbService = require("moleculer-db");
|
||||||
|
const MongoAdapter = require("moleculer-db-adapter-mongoose");
|
||||||
|
|
||||||
|
export const DbMixin = (collection, model) => {
|
||||||
|
if(process.env.MONGO_URI) {
|
||||||
|
return {
|
||||||
|
mixins: [DbService],
|
||||||
|
adapter: new MongoAdapter(process.env.MONGO_URI, {
|
||||||
|
user: process.env.MONGO_INITDB_ROOT_USERNAME,
|
||||||
|
pass: process.env.MONGO_INITDB_ROOT_PASSWORD,
|
||||||
|
keepAlive: true,
|
||||||
|
}),
|
||||||
|
model: model,
|
||||||
|
collection
|
||||||
|
};
|
||||||
|
}
|
||||||
|
mkdir(path.resolve("./data"));
|
||||||
|
};
|
||||||
67
models/comic.model.ts
Normal file
67
models/comic.model.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const mongoose = require("mongoose");
|
||||||
|
const paginate = require("mongoose-paginate");
|
||||||
|
|
||||||
|
const ComicSchema = mongoose.Schema({
|
||||||
|
name: String,
|
||||||
|
type: String,
|
||||||
|
import: {
|
||||||
|
isImported: Boolean,
|
||||||
|
matchedResult: {
|
||||||
|
score: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userAddedMetadata: {
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
comicStructure: {
|
||||||
|
cover: {
|
||||||
|
thumb: String,
|
||||||
|
medium: String,
|
||||||
|
large: String,
|
||||||
|
},
|
||||||
|
collection: {
|
||||||
|
publishDate: String,
|
||||||
|
type: String, // issue, series, trade paperback
|
||||||
|
metadata: {
|
||||||
|
publisher: String,
|
||||||
|
issueNumber: String,
|
||||||
|
description: String,
|
||||||
|
synopsis: String,
|
||||||
|
team: {
|
||||||
|
writer: String,
|
||||||
|
inker: String,
|
||||||
|
penciler: String,
|
||||||
|
colorist: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourcedMetadata: {
|
||||||
|
comicvine: {},
|
||||||
|
shortboxed: {},
|
||||||
|
gcd: {},
|
||||||
|
},
|
||||||
|
rawFileDetails: {
|
||||||
|
fileName: String,
|
||||||
|
path: String,
|
||||||
|
extension: String,
|
||||||
|
},
|
||||||
|
acquisition: {
|
||||||
|
wanted: Boolean,
|
||||||
|
release: {},
|
||||||
|
torrent: {
|
||||||
|
sourceApplication: String,
|
||||||
|
magnet: String,
|
||||||
|
tracker: String,
|
||||||
|
status: String,
|
||||||
|
},
|
||||||
|
usenet: {
|
||||||
|
sourceApplication: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
ComicSchema.plugin(paginate);
|
||||||
|
const Comic = mongoose.model("Comic", ComicSchema);
|
||||||
|
export default Comic;
|
||||||
187
moleculer.config.ts
Normal file
187
moleculer.config.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"use strict";
|
||||||
|
import {BrokerOptions, Errors, MetricRegistry, ServiceBroker} from "moleculer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moleculer ServiceBroker configuration file
|
||||||
|
*
|
||||||
|
* More info about options:
|
||||||
|
* https://moleculer.services/docs/0.14/configuration.html
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Overwriting options in production:
|
||||||
|
* ================================
|
||||||
|
* You can overwrite any option with environment variables.
|
||||||
|
* For example to overwrite the "logLevel" value, use `LOGLEVEL=warn` env var.
|
||||||
|
* To overwrite a nested parameter, e.g. retryPolicy.retries, use `RETRYPOLICY_RETRIES=10` env var.
|
||||||
|
*
|
||||||
|
* To overwrite broker’s deeply nested default options, which are not presented in "moleculer.config.js",
|
||||||
|
* use the `MOL_` prefix and double underscore `__` for nested properties in .env file.
|
||||||
|
* For example, to set the cacher prefix to `MYCACHE`, you should declare an env var as `MOL_CACHER__OPTIONS__PREFIX=mycache`.
|
||||||
|
* It will set this:
|
||||||
|
* {
|
||||||
|
* cacher: {
|
||||||
|
* options: {
|
||||||
|
* prefix: "mycache"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const brokerConfig: BrokerOptions = {
|
||||||
|
// Namespace of nodes to segment your nodes on the same network.
|
||||||
|
namespace: "",
|
||||||
|
// Unique node identifier. Must be unique in a namespace.
|
||||||
|
nodeID: null,
|
||||||
|
// Custom metadata store. Store here what you want. Accessing: `this.broker.metadata`
|
||||||
|
metadata: {},
|
||||||
|
|
||||||
|
// Enable/disable logging or use custom logger. More info: https://moleculer.services/docs/0.14/logging.html
|
||||||
|
// Available logger types: "Console", "File", "Pino", "Winston", "Bunyan", "debug", "Log4js", "Datadog"
|
||||||
|
logger: {
|
||||||
|
type: "Console",
|
||||||
|
options: {
|
||||||
|
// Using colors on the output
|
||||||
|
colors: true,
|
||||||
|
// Print module names with different colors (like docker-compose for containers)
|
||||||
|
moduleColors: false,
|
||||||
|
// Line formatter. It can be "json", "short", "simple", "full", a `Function` or a template string like "{timestamp} {level} {nodeID}/{mod}: {msg}"
|
||||||
|
formatter: "full",
|
||||||
|
// Custom object printer. If not defined, it uses the `util.inspect` method.
|
||||||
|
objectPrinter: null,
|
||||||
|
// Auto-padding the module name in order to messages begin at the same column.
|
||||||
|
autoPadding: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Default log level for built-in console logger. It can be overwritten in logger options above.
|
||||||
|
// Available values: trace, debug, info, warn, error, fatal
|
||||||
|
logLevel: "info",
|
||||||
|
|
||||||
|
// Define transporter.
|
||||||
|
// More info: https://moleculer.services/docs/0.14/networking.html
|
||||||
|
// Note: During the development, you don't need to define it because all services will be loaded locally.
|
||||||
|
// In production you can set it via `TRANSPORTER=nats://localhost:4222` environment variable.
|
||||||
|
transporter: null, // "NATS"
|
||||||
|
|
||||||
|
// Define a cacher.
|
||||||
|
// More info: https://moleculer.services/docs/0.14/caching.html
|
||||||
|
cacher: "Memory",
|
||||||
|
|
||||||
|
// Define a serializer.
|
||||||
|
// Available values: "JSON", "Avro", "ProtoBuf", "MsgPack", "Notepack", "Thrift".
|
||||||
|
// More info: https://moleculer.services/docs/0.14/networking.html#Serialization
|
||||||
|
serializer: "JSON",
|
||||||
|
|
||||||
|
// Number of milliseconds to wait before reject a request with a RequestTimeout error. Disabled: 0
|
||||||
|
requestTimeout: 10 * 1000,
|
||||||
|
|
||||||
|
// Retry policy settings. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Retry
|
||||||
|
retryPolicy: {
|
||||||
|
// Enable feature
|
||||||
|
enabled: false,
|
||||||
|
// Count of retries
|
||||||
|
retries: 5,
|
||||||
|
// First delay in milliseconds.
|
||||||
|
delay: 100,
|
||||||
|
// Maximum delay in milliseconds.
|
||||||
|
maxDelay: 1000,
|
||||||
|
// Backoff factor for delay. 2 means exponential backoff.
|
||||||
|
factor: 2,
|
||||||
|
// A function to check failed requests.
|
||||||
|
check: (err: Errors.MoleculerError) => err && !!err.retryable,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection)
|
||||||
|
maxCallLevel: 100,
|
||||||
|
|
||||||
|
// Number of seconds to send heartbeat packet to other nodes.
|
||||||
|
heartbeatInterval: 10,
|
||||||
|
// Number of seconds to wait before setting node to unavailable status.
|
||||||
|
heartbeatTimeout: 30,
|
||||||
|
|
||||||
|
// Cloning the params of context if enabled. High performance impact, use it with caution!
|
||||||
|
contextParamsCloning: false,
|
||||||
|
|
||||||
|
// Tracking requests and waiting for running requests before shuting down. More info: https://moleculer.services/docs/0.14/context.html#Context-tracking
|
||||||
|
tracking: {
|
||||||
|
// Enable feature
|
||||||
|
enabled: false,
|
||||||
|
// Number of milliseconds to wait before shuting down the process.
|
||||||
|
shutdownTimeout: 5000,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Disable built-in request & emit balancer. (Transporter must support it, as well.). More info: https://moleculer.services/docs/0.14/networking.html#Disabled-balancer
|
||||||
|
disableBalancer: false,
|
||||||
|
|
||||||
|
// Settings of Service Registry. More info: https://moleculer.services/docs/0.14/registry.html
|
||||||
|
registry: {
|
||||||
|
// Define balancing strategy. More info: https://moleculer.services/docs/0.14/balancing.html
|
||||||
|
// Available values: "RoundRobin", "Random", "CpuUsage", "Latency", "Shard"
|
||||||
|
strategy: "RoundRobin",
|
||||||
|
// Enable local action call preferring. Always call the local action instance if available.
|
||||||
|
preferLocal: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Settings of Circuit Breaker. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Circuit-Breaker
|
||||||
|
circuitBreaker: {
|
||||||
|
// Enable feature
|
||||||
|
enabled: false,
|
||||||
|
// Threshold value. 0.5 means that 50% should be failed for tripping.
|
||||||
|
threshold: 0.5,
|
||||||
|
// Minimum request count. Below it, CB does not trip.
|
||||||
|
minRequestCount: 20,
|
||||||
|
// Number of seconds for time window.
|
||||||
|
windowTime: 60,
|
||||||
|
// Number of milliseconds to switch from open to half-open state
|
||||||
|
halfOpenTime: 10 * 1000,
|
||||||
|
// A function to check failed requests.
|
||||||
|
check: (err: Errors.MoleculerError) => err && err.code >= 500,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead
|
||||||
|
bulkhead: {
|
||||||
|
// Enable feature.
|
||||||
|
enabled: false,
|
||||||
|
// Maximum concurrent executions.
|
||||||
|
concurrency: 10,
|
||||||
|
// Maximum size of queue
|
||||||
|
maxQueueSize: 100,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html
|
||||||
|
validator: true,
|
||||||
|
|
||||||
|
errorHandler: null,
|
||||||
|
|
||||||
|
// Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html
|
||||||
|
metrics: {
|
||||||
|
enabled: false,
|
||||||
|
// Available built-in reporters: "Console", "CSV", "Event", "Prometheus", "Datadog", "StatsD"
|
||||||
|
reporter: {
|
||||||
|
type: "Console",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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!
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Register custom middlewares
|
||||||
|
middlewares: [],
|
||||||
|
|
||||||
|
// Register custom REPL commands.
|
||||||
|
replCommands: null,
|
||||||
|
/*
|
||||||
|
// Called after broker created.
|
||||||
|
created : (broker: ServiceBroker): void => {},
|
||||||
|
// Called after broker started.
|
||||||
|
started: async (broker: ServiceBroker): Promise<void> => {},
|
||||||
|
stopped: async (broker: ServiceBroker): Promise<void> => {},
|
||||||
|
*/
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export = brokerConfig;
|
||||||
21209
package-lock.json
generated
Normal file
21209
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
79
package.json
Normal file
79
package.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"name": "threetwo-import-service",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "My Moleculer-based microservices project",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build tsconfig.json",
|
||||||
|
"dev": "ts-node ./node_modules/moleculer/bin/moleculer-runner.js --hot --repl --config moleculer.config.ts services/**/*.service.ts",
|
||||||
|
"start": "moleculer-runner --config dist/moleculer.config.js",
|
||||||
|
"cli": "moleculer connect NATS",
|
||||||
|
"ci": "jest --watch",
|
||||||
|
"test": "jest --coverage",
|
||||||
|
"lint": "eslint --ext .js,.ts ."
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"microservices",
|
||||||
|
"moleculer"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.14.168",
|
||||||
|
"@types/unzipper": "^0.10.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-import": "^2.20.2",
|
||||||
|
"eslint-plugin-prefer-arrow": "^1.2.2",
|
||||||
|
"jest": "^25.1.0",
|
||||||
|
"jest-cli": "^25.1.0",
|
||||||
|
"moleculer-repl": "^0.6.2",
|
||||||
|
"ts-jest": "^25.3.0",
|
||||||
|
"ts-node": "^8.8.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@root/walk": "^1.1.0",
|
||||||
|
"@types/jest": "^25.1.4",
|
||||||
|
"@types/mkdirp": "^1.0.0",
|
||||||
|
"@types/node": "^13.9.8",
|
||||||
|
"@types/pino": "^6.3.8",
|
||||||
|
"fs-extra": "^10.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"moleculer": "^0.14.0",
|
||||||
|
"moleculer-db": "^0.8.4",
|
||||||
|
"moleculer-db-adapter-mongo": "^0.4.7",
|
||||||
|
"moleculer-db-adapter-mongoose": "^0.8.9",
|
||||||
|
"moleculer-web": "^0.9.0",
|
||||||
|
"mongoose": "^5.12.7",
|
||||||
|
"mongoose-paginate": "^5.0.3",
|
||||||
|
"nats": "^1.3.2",
|
||||||
|
"node-unrar-js": "^1.0.2",
|
||||||
|
"pino": "^6.11.3",
|
||||||
|
"pino-pretty": "^4.7.1",
|
||||||
|
"sharp": "^0.28.1",
|
||||||
|
"typescript": "^3.8.3",
|
||||||
|
"unzipper": "^0.10.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.x.x"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"coverageDirectory": "<rootDir>/coverage",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"ts",
|
||||||
|
"tsx",
|
||||||
|
"js"
|
||||||
|
],
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||||
|
},
|
||||||
|
"testMatch": [
|
||||||
|
"**/*.spec.(ts|js)"
|
||||||
|
],
|
||||||
|
"globals": {
|
||||||
|
"ts-jest": {
|
||||||
|
"tsConfig": "tsconfig.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/banner.png
Normal file
BIN
public/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
754
public/index.html
Normal file
754
public/index.html
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,minimal-ui">
|
||||||
|
<title>threetwo-import-service - Moleculer Microservices Project</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700" rel="stylesheet">
|
||||||
|
<link href="https://unpkg.com/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
|
||||||
|
<link rel="shortcut icon" type="image/png" href="https://moleculer.services/icon/favicon-16x16.png"/>
|
||||||
|
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
|
||||||
|
<style type="text/css">
|
||||||
|
html {
|
||||||
|
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: black;
|
||||||
|
text-shadow: 1px 1px 3px rgba(0,0,0,0.2);
|
||||||
|
padding-bottom: 60px; /* footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header, footer {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 1px 1px 3px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
header a, header a.router-link-exact-active,
|
||||||
|
footer a, footer a.router-link-exact-active
|
||||||
|
{
|
||||||
|
color: #63dcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-image: linear-gradient(45deg, #e37682 0%, #5f4d93 100%);
|
||||||
|
padding: 1em;
|
||||||
|
box-shadow: 0 3px 10px rgba(0,0,0,0.6);
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
background-image: linear-gradient(135deg, #e37682 0%, #5f4d93 100%);
|
||||||
|
padding: 0.75em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
box-shadow: 0 -3px 10px rgba(0,0,0,0.6);
|
||||||
|
position: fixed;
|
||||||
|
left: 0; right: 0; bottom: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer .footer-links {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer .footer-links a {
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a.router-link-exact-active {
|
||||||
|
color: #3CAFCE;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25em 0.75em;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 1.25em;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
transition: color .1s linear, border-bottom .1s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li.active {
|
||||||
|
border-bottom: 2px solid #63dcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul li:hover {
|
||||||
|
color: #63dcfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .button {
|
||||||
|
background-color: #3CAFCE;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,.2);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button i, .button i {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover, .button:hover {
|
||||||
|
background-color: #4ba3bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "Consolas", 'Courier New', Courier, monospace;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 1260px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main section#home > .content {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main section#home h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main section#home h3 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.broker-options {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxes .box {
|
||||||
|
width: 200px;
|
||||||
|
padding: 0.25em 1em;
|
||||||
|
margin: 0.5em;
|
||||||
|
background: rgba(60, 175, 206, 0.1);
|
||||||
|
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxes .box .caption {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boxes .box .value {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main input {
|
||||||
|
border: 1px solid #3CAFCE;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-family: "Source Sans Pro";
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset {
|
||||||
|
border: 1px solid lightgrey;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 2px 2px 10px rgba(0,0,0,0.4);
|
||||||
|
background-color: aliceblue;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset legend {
|
||||||
|
background-color: #cce7ff;
|
||||||
|
border: 1px solid lightgrey;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset .content {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset .request {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset .parameters .field {
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset .parameters .field label {
|
||||||
|
min-width: 80px;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset .response {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main fieldset .response pre {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.json .string { color: #885800; }
|
||||||
|
pre.json .number { color: blue; }
|
||||||
|
pre.json .boolean { color: magenta; }
|
||||||
|
pre.json .null { color: red; }
|
||||||
|
pre.json .key { color: green; }
|
||||||
|
|
||||||
|
main h4 {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0.25em -1.0em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: dimgray;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 7px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.green {
|
||||||
|
background-color: limegreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.red {
|
||||||
|
background-color: firebrick;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.orange {
|
||||||
|
background-color: #fab000;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
/*max-width: 1000px;*/
|
||||||
|
border: 1px solid lightgrey;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: aliceblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: #cce7ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr.offline td {
|
||||||
|
font-style: italic;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr.local td {
|
||||||
|
/*color: blue;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr:not(:last-child) td {
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th:nth-child(1), table td:nth-child(1) {
|
||||||
|
text-align: left
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr.service td:nth-child(1) {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr.action td:nth-child(1) {
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td:nth-child(2) {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; right: 0; top: 0; bottom: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<header>
|
||||||
|
<a href="https://moleculer.services/docs/0.14/" target="_blank"><img class="logo" src="https://moleculer.services/images/logo/logo_with_text_horizontal_100h_shadow.png" /></a>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in menu" :class="{ active: page == item.id}" @click="changePage(item.id)">{{ item.caption }}</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section id="home" v-if="page == 'home'">
|
||||||
|
<div class="content">
|
||||||
|
<h1>Welcome to your Moleculer microservices project!</h1>
|
||||||
|
<p>Check out the <a href="https://moleculer.services/docs/0.14/" target="_blank">Moleculer documentation</a> to learn how to customize this project.</p>
|
||||||
|
|
||||||
|
<template v-if="broker">
|
||||||
|
<h3>Configuration</h3>
|
||||||
|
<div class="boxes">
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Namespace</div>
|
||||||
|
<div class="value">{{ broker.namespace || "<not set>" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Transporter</div>
|
||||||
|
<div class="value">{{ broker.transporter || "<no transporter>" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Serializer</div>
|
||||||
|
<div class="value">{{ broker.serializer || "JSON" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Strategy</div>
|
||||||
|
<div class="value">{{ broker.registry.strategy || "Round Robin" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Cacher</div>
|
||||||
|
<div class="value">{{ broker.cacher ? "Enabled" : "Disabled" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Logger</div>
|
||||||
|
<div class="value">{{ broker.logger ? "Enabled" : "Disabled" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Metrics</div>
|
||||||
|
<div class="value">{{ broker.metrics.enabled ? "Enabled" : "Disabled" }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="caption">Tracing</div>
|
||||||
|
<div class="value">{{ broker.tracing.enabled ? "Enabled" : "Disabled" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="cursor-pointer" @click="showBrokerOptions = !showBrokerOptions">Broker options <i :class="'fa fa-angle-' + (showBrokerOptions ? 'up' : 'down')"></i></h3>
|
||||||
|
<pre v-if="showBrokerOptions" class="broker-options"><code>{{ broker }}</code></pre>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<template v-for="(section, name) in requests">
|
||||||
|
<section :id="name" v-if="page == name">
|
||||||
|
<fieldset v-for="item in section">
|
||||||
|
<legend>
|
||||||
|
Action '<code>{{ item.action }}</code>'
|
||||||
|
</legend>
|
||||||
|
<div class="content">
|
||||||
|
<div class="request">
|
||||||
|
<h4>Request:</h4>
|
||||||
|
<code>{{ item.method || 'GET' }} <a target="_blank" :href="item.rest">{{ item.rest }} </a></code>
|
||||||
|
<a class="button" @click="callAction(item)">
|
||||||
|
<i class="fa fa-rocket"></i>
|
||||||
|
Execute
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.fields" class="parameters">
|
||||||
|
<h4>Parameters:</h4>
|
||||||
|
<div class="field" v-for="field in item.fields">
|
||||||
|
<label for="">{{ field.label }}: </label>
|
||||||
|
<input :type="field.type" :value="getFieldValue(field)" @input="setFieldValue(field, $event.target.value)"></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="response" v-if="item.status">
|
||||||
|
<h4>Response:
|
||||||
|
<div class="badge" :class="{ green: item.status < 400, red: item.status >= 400 || item.status == 'ERR' }">{{ item.status }}</div>
|
||||||
|
<div class="badge time">{{ humanize(item.duration) }}</div>
|
||||||
|
</h4>
|
||||||
|
<pre><code>{{ item.response }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<section id="nodes" v-if="page == 'nodes'">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Node ID</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Hostname</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>CPU</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="node in nodes" :class="{ offline: !node.available, local: node.local }" :key="node.id">
|
||||||
|
<td>{{ node.id }}</td>
|
||||||
|
<td>{{ node.client.type }}</td>
|
||||||
|
<td>{{ node.client.version }}</td>
|
||||||
|
<td>{{ node.ipList[0] }}</td>
|
||||||
|
<td>{{ node.hostname }}</td>
|
||||||
|
|
||||||
|
<td><div class="badge" :class="{ green: node.available, red: !node.available }">{{ node.available ? "Online": "Offline" }}</div></td>
|
||||||
|
<td>
|
||||||
|
<div class="bar" :style="{ width: node.cpu != null ? node.cpu + '%' : '0' }"></div>
|
||||||
|
{{ node.cpu != null ? Number(node.cpu).toFixed(0) + '%' : '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
<section id="services" v-if="page == 'services'">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<th>Service/Action name</th>
|
||||||
|
<th>REST</th>
|
||||||
|
<th>Parameters</th>
|
||||||
|
<th>Instances</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="svc in filteredServices">
|
||||||
|
<tr class="service">
|
||||||
|
<td>
|
||||||
|
{{ svc.name }}
|
||||||
|
<div v-if="svc.version" class="badge">{{ svc.version }}</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ svc.settings.rest ? svc.settings.rest : svc.fullName }}</td>
|
||||||
|
<td></td>
|
||||||
|
<td class="badges">
|
||||||
|
<div class="badge" v-for="nodeID in svc.nodes">
|
||||||
|
{{ nodeID }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="svc.nodes.length > 0" class="badge green">Online</div>
|
||||||
|
<div v-else class="badge red">Offline</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="action in getServiceActions(svc)" :class="{ action: true, offline: !action.available, local: action.hasLocal }">
|
||||||
|
<td>
|
||||||
|
{{ action.name }}
|
||||||
|
<div v-if="action.action.cache" class="badge orange">cached</div>
|
||||||
|
</td>
|
||||||
|
<td v-html="getActionREST(svc, action)"></td>
|
||||||
|
<td :title="getActionParams(action)">
|
||||||
|
{{ getActionParams(action, 40) }}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<div v-if="action.available" class="badge green">Online</div>
|
||||||
|
<div v-else class="badge red">Offline</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="footer-copyright">
|
||||||
|
Copyright © 2016-2020 - Moleculer
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="https://github.com/moleculerjs/moleculer" class="footer-link" target="_blank">Github</a>
|
||||||
|
<a href="https://twitter.com/MoleculerJS" class="footer-link" target="_blank">Twitter</a>
|
||||||
|
<a href="https://discord.gg/TSEcDRP" class="footer-link" target="_blank">Discord</a>
|
||||||
|
<a href="https://stackoverflow.com/questions/tagged/moleculer" class="footer-link" target="_blank">Stack Overflow</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var app = new Vue({
|
||||||
|
el: "#app",
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: [
|
||||||
|
{ id: "home", caption: "Home" },
|
||||||
|
{ id: "greeter", caption: "Greeter service" },
|
||||||
|
{ id: "products", caption: "Products service" },
|
||||||
|
{ id: "nodes", caption: "Nodes" },
|
||||||
|
{ id: "services", caption: "Services" }
|
||||||
|
],
|
||||||
|
page: "home",
|
||||||
|
|
||||||
|
requests: {
|
||||||
|
greeter: [
|
||||||
|
{ id: "hello", action: "greeter.hello", rest: "/api/greeter/hello", response: null, status: null, duration: null },
|
||||||
|
|
||||||
|
{ id: "welcome", action: "greeter.welcome", rest: "/api/greeter/welcome", fields: [
|
||||||
|
{ field: "name", label: "Name", type: "text", paramType: "param", model: "welcomeName" }
|
||||||
|
], response: null, status: null, duration: null }
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
{ id: "list", action: "products.list", rest: "/api/products/", response: null, status: null, duration: null, afterResponse: response => !this.fields.productID && (this.fields.productID = response.rows[0]._id) },
|
||||||
|
|
||||||
|
{ id: "create", action: "products.create", rest: "/api/products/", method: "POST", fields: [
|
||||||
|
{ field: "name", label: "Name", type: "text", paramType: "body", model: "productName" },
|
||||||
|
{ field: "price", label: "Price", type: "number", paramType: "body", model: "productPrice" },
|
||||||
|
], response: null, status: null, duration: null, afterResponse: response => this.fields.productID = response._id },
|
||||||
|
|
||||||
|
{ id: "get", action: "products.get", rest: "/api/products/:id", method: "GET", fields: [
|
||||||
|
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" }
|
||||||
|
], response: null, status: null, duration: null },
|
||||||
|
|
||||||
|
{ id: "update", action: "products.update", rest: "/api/products/:id", method: "PUT", fields: [
|
||||||
|
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" },
|
||||||
|
{ field: "name", label: "Name", type: "text", paramType: "body", model: "productName" },
|
||||||
|
{ field: "price", label: "Price", type: "number", paramType: "body", model: "productPrice" },
|
||||||
|
], response: null, status: null, duration: null },
|
||||||
|
|
||||||
|
{ id: "increase", action: "products.increaseQuantity", rest: "/api/products/:id/quantity/increase", method: "PUT", fields: [
|
||||||
|
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" },
|
||||||
|
{ field: "value", label: "Value", type: "number", paramType: "body", model: "productValue" },
|
||||||
|
], response: null, status: null, duration: null },
|
||||||
|
|
||||||
|
{ id: "decrease", action: "products.decreaseQuantity", rest: "/api/products/:id/quantity/decrease", method: "PUT", fields: [
|
||||||
|
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" },
|
||||||
|
{ field: "value", label: "Value", type: "number", paramType: "body", model: "productValue" },
|
||||||
|
], response: null, status: null, duration: null },
|
||||||
|
|
||||||
|
{ id: "delete", action: "products.delete", rest: "/api/products/:id", method: "DELETE", fields: [
|
||||||
|
{ field: "id", label: "ID", type: "text", paramType: "url", model: "productID" }
|
||||||
|
], response: null, status: null, duration: null },
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: {
|
||||||
|
welcomeName: "John",
|
||||||
|
productID: null,
|
||||||
|
productName: "Xiamoi Mi 9T",
|
||||||
|
productPrice: 299,
|
||||||
|
productValue: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
broker: null,
|
||||||
|
nodes: [],
|
||||||
|
services: [],
|
||||||
|
actions: {},
|
||||||
|
|
||||||
|
showBrokerOptions: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filteredServices() {
|
||||||
|
return this.services.filter(svc => !svc.name.startsWith("$"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
changePage(page) {
|
||||||
|
this.page = page;
|
||||||
|
this.updatePageResources();
|
||||||
|
},
|
||||||
|
|
||||||
|
humanize(ms) {
|
||||||
|
return ms > 1500 ? (ms / 1500).toFixed(2) + " s" : ms + " ms";
|
||||||
|
},
|
||||||
|
|
||||||
|
getFieldValue(field) {
|
||||||
|
return this.fields[field.model];
|
||||||
|
},
|
||||||
|
|
||||||
|
setFieldValue(field, newValue) {
|
||||||
|
if (field.type == "number")
|
||||||
|
this.fields[field.model] = Number(newValue);
|
||||||
|
else
|
||||||
|
this.fields[field.model] = newValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
getServiceActions(svc) {
|
||||||
|
return Object.keys(svc.actions)
|
||||||
|
.map(name => this.actions[name])
|
||||||
|
.filter(action => !!action);
|
||||||
|
},
|
||||||
|
|
||||||
|
getActionParams(action, maxLen) {
|
||||||
|
if (action.action && action.action.params) {
|
||||||
|
const s = Object.keys(action.action.params).join(", ");
|
||||||
|
return s.length > maxLen ? s.substr(0, maxLen) + "…" : s;
|
||||||
|
}
|
||||||
|
return "-";
|
||||||
|
},
|
||||||
|
|
||||||
|
getActionREST(svc, action) {
|
||||||
|
if (action.action.rest) {
|
||||||
|
let prefix = svc.fullName || svc.name;
|
||||||
|
if (typeof(svc.settings.rest) == "string")
|
||||||
|
prefix = svc.settings.rest;
|
||||||
|
|
||||||
|
if (typeof action.action.rest == "string") {
|
||||||
|
if (action.action.rest.indexOf(" ") !== -1) {
|
||||||
|
const p = action.action.rest.split(" ");
|
||||||
|
return "<span class='badge'>" + p[0] + "</span> " + prefix + p[1];
|
||||||
|
} else {
|
||||||
|
return "<span class='badge'>*</span> " + prefix + action.action.rest;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "<span class='badge'>" + (action.action.rest.method || "*") + "</span> " + prefix + action.action.rest.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
callAction: function (item) {
|
||||||
|
var startTime = Date.now();
|
||||||
|
|
||||||
|
let url = item.rest;
|
||||||
|
const method = item.method || "GET";
|
||||||
|
let body = null;
|
||||||
|
let params = null;
|
||||||
|
if (item.fields) {
|
||||||
|
body = {};
|
||||||
|
params = {};
|
||||||
|
item.fields.forEach(field => {
|
||||||
|
const value = this.getFieldValue(field);
|
||||||
|
if (field.paramType == "body")
|
||||||
|
body[field.field] = value;
|
||||||
|
else if (field.paramType == "param")
|
||||||
|
params[field.field] = value;
|
||||||
|
else if (field.paramType == "url")
|
||||||
|
url = url.replace(":" + field.field, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (body && method == "GET") {
|
||||||
|
body = null;
|
||||||
|
}
|
||||||
|
if (params) {
|
||||||
|
url += "?" + new URLSearchParams(params).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
method,
|
||||||
|
body: body ? JSON.stringify(body) : null,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(function(res) {
|
||||||
|
item.status = res.status;
|
||||||
|
item.duration = Date.now() - startTime;
|
||||||
|
return res.json().then(json => {
|
||||||
|
item.response = json;
|
||||||
|
if (item.afterResponse)
|
||||||
|
return item.afterResponse(json);
|
||||||
|
});
|
||||||
|
}).catch(function (err) {
|
||||||
|
item.status = "ERR";
|
||||||
|
item.duration = Date.now() - startTime;
|
||||||
|
item.response = err.message;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBrokerOptions: function (name) {
|
||||||
|
this.req("/api/~node/options", null).then(res => this.broker = res);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateNodeList: function (name) {
|
||||||
|
this.req("/api/~node/list", null).then(res => {
|
||||||
|
res.sort((a,b) => a.id.localeCompare(b.id));
|
||||||
|
this.nodes = res;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateServiceList: function (name) {
|
||||||
|
this.req("/api/~node/services?withActions=true", null)
|
||||||
|
.then(res => {
|
||||||
|
this.services = res;
|
||||||
|
res.sort((a,b) => a.name.localeCompare(b.name));
|
||||||
|
res.forEach(svc => svc.nodes.sort());
|
||||||
|
})
|
||||||
|
.then(() => this.req("/api/~node/actions", null))
|
||||||
|
.then(res => {
|
||||||
|
res.sort((a,b) => a.name.localeCompare(b.name));
|
||||||
|
const actions = res.reduce((a,b) => {
|
||||||
|
a[b.name] = b;
|
||||||
|
return a;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
Vue.set(this, "actions", actions);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
req: function (url, params) {
|
||||||
|
return fetch(url, { method: "GET", body: params ? JSON.stringify(params) : null })
|
||||||
|
.then(function(res) {
|
||||||
|
return res.json();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePageResources() {
|
||||||
|
if (this.page == 'nodes') return this.updateNodeList();
|
||||||
|
if (this.page == 'services') return this.updateServiceList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
var self = this;
|
||||||
|
setInterval(function () {
|
||||||
|
self.updatePageResources();
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
this.updateBrokerOptions();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
174
services/api.service.ts
Normal file
174
services/api.service.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import {IncomingMessage} from "http";
|
||||||
|
import {Service, ServiceBroker, Context} from "moleculer";
|
||||||
|
import ApiGateway from "moleculer-web";
|
||||||
|
|
||||||
|
export default class ApiService extends Service {
|
||||||
|
|
||||||
|
public constructor(broker: ServiceBroker) {
|
||||||
|
super(broker);
|
||||||
|
// @ts-ignore
|
||||||
|
this.parseServiceSchema({
|
||||||
|
name: "api",
|
||||||
|
mixins: [ApiGateway],
|
||||||
|
// More info about settings: https://moleculer.services/docs/0.14/moleculer-web.html
|
||||||
|
settings: {
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
|
||||||
|
routes: [{
|
||||||
|
path: "/api",
|
||||||
|
whitelist: [
|
||||||
|
// Access to any actions in all services under "/api" URL
|
||||||
|
"**",
|
||||||
|
],
|
||||||
|
// Route-level Express middlewares. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Middlewares
|
||||||
|
use: [],
|
||||||
|
// Enable/disable parameter merging method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Disable-merging
|
||||||
|
mergeParams: true,
|
||||||
|
|
||||||
|
// Enable authentication. Implement the logic into `authenticate` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authentication
|
||||||
|
authentication: false,
|
||||||
|
|
||||||
|
// Enable authorization. Implement the logic into `authorize` method. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Authorization
|
||||||
|
authorization: false,
|
||||||
|
|
||||||
|
// The auto-alias feature allows you to declare your route alias directly in your services.
|
||||||
|
// The gateway will dynamically build the full routes from service schema.
|
||||||
|
autoAliases: true,
|
||||||
|
|
||||||
|
aliases:{},
|
||||||
|
/**
|
||||||
|
* Before call hook. You can check the request.
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {Object} route
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @param {Object} data
|
||||||
|
onBeforeCall(ctx: Context<any,{userAgent: string}>,
|
||||||
|
route: object, req: IncomingMessage, res: ServerResponse) {
|
||||||
|
Set request headers to context meta
|
||||||
|
ctx.meta.userAgent = req.headers["user-agent"];
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After call hook. You can modify the data.
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {Object} route
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @param {ServerResponse} res
|
||||||
|
* @param {Object} data
|
||||||
|
*
|
||||||
|
onAfterCall(ctx: Context, route: object, req: IncomingMessage, res: ServerResponse, data: object) {
|
||||||
|
// Async function which return with Promise
|
||||||
|
return doSomething(ctx, res, data);
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Calling options. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Calling-options
|
||||||
|
callingOptions: {},
|
||||||
|
|
||||||
|
bodyParsers: {
|
||||||
|
json: {
|
||||||
|
strict: false,
|
||||||
|
limit: "1MB",
|
||||||
|
},
|
||||||
|
urlencoded: {
|
||||||
|
extended: true,
|
||||||
|
limit: "1MB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mapping policy setting. More info: https://moleculer.services/docs/0.14/moleculer-web.html#Mapping-policy
|
||||||
|
mappingPolicy: "all", // Available values: "all", "restrict"
|
||||||
|
|
||||||
|
// Enable/disable logging
|
||||||
|
logging: true,
|
||||||
|
}],
|
||||||
|
// Do not log client side errors (does not log an error response when the error.code is 400<=X<500)
|
||||||
|
log4XXResponses: false,
|
||||||
|
// Logging the request parameters. Set to any log level to enable it. E.g. "info"
|
||||||
|
logRequestParams: null,
|
||||||
|
// Logging the response data. Set to any log level to enable it. E.g. "info"
|
||||||
|
logResponseData: null,
|
||||||
|
// Serve assets from "public" folder
|
||||||
|
assets: {
|
||||||
|
folder: "public",
|
||||||
|
// Options to `server-static` module
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate the request. It checks the `Authorization` token value in the request header.
|
||||||
|
* Check the token value & resolve the user by the token.
|
||||||
|
* The resolved user will be available in `ctx.meta.user`
|
||||||
|
*
|
||||||
|
* PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION!
|
||||||
|
*
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {any} route
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @returns {Promise}
|
||||||
|
|
||||||
|
async authenticate (ctx: Context, route: any, req: IncomingMessage): Promise < any > => {
|
||||||
|
// Read the token from header
|
||||||
|
const auth = req.headers.authorization;
|
||||||
|
|
||||||
|
if (auth && auth.startsWith("Bearer")) {
|
||||||
|
const token = auth.slice(7);
|
||||||
|
|
||||||
|
// Check the token. Tip: call a service which verify the token. E.g. `accounts.resolveToken`
|
||||||
|
if (token === "123456") {
|
||||||
|
// Returns the resolved user. It will be set to the `ctx.meta.user`
|
||||||
|
return {
|
||||||
|
id: 1,
|
||||||
|
name: "John Doe",
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Invalid token
|
||||||
|
throw new ApiGateway.Errors.UnAuthorizedError(ApiGateway.Errors.ERR_INVALID_TOKEN, {
|
||||||
|
error: "Invalid Token",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// No token. Throw an error or do nothing if anonymous access is allowed.
|
||||||
|
// Throw new E.UnAuthorizedError(E.ERR_NO_TOKEN);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize the request. Check that the authenticated user has right to access the resource.
|
||||||
|
*
|
||||||
|
* PLEASE NOTE, IT'S JUST AN EXAMPLE IMPLEMENTATION. DO NOT USE IN PRODUCTION!
|
||||||
|
*
|
||||||
|
* @param {Context} ctx
|
||||||
|
* @param {Object} route
|
||||||
|
* @param {IncomingMessage} req
|
||||||
|
* @returns {Promise}
|
||||||
|
|
||||||
|
async authorize (ctx: Context < any, {
|
||||||
|
user: string;
|
||||||
|
} > , route: Record<string, undefined>, req: IncomingMessage): Promise < any > => {
|
||||||
|
// Get the authenticated user.
|
||||||
|
const user = ctx.meta.user;
|
||||||
|
|
||||||
|
// It check the `auth` property in action schema.
|
||||||
|
// @ts-ignore
|
||||||
|
if (req.$action.auth === "required" && !user) {
|
||||||
|
throw new ApiGateway.Errors.UnAuthorizedError("NO_RIGHTS", {
|
||||||
|
error: "Unauthorized",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
80
services/import.service.ts
Normal file
80
services/import.service.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use strict";
|
||||||
|
import { Context, Service, ServiceBroker, ServiceSchema } from "moleculer";
|
||||||
|
|
||||||
|
import { DbMixin } from "../mixins/db.mixin";
|
||||||
|
import Comic from "../models/comic.model";
|
||||||
|
import {
|
||||||
|
walkFolder,
|
||||||
|
extractArchive,
|
||||||
|
getCovers,
|
||||||
|
} from "../utils/uncompression.utils";
|
||||||
|
import {
|
||||||
|
IExtractionOptions,
|
||||||
|
IFolderData,
|
||||||
|
IFolderResponse,
|
||||||
|
} from "../interfaces/folder.interface";
|
||||||
|
|
||||||
|
export default class ProductsService extends Service {
|
||||||
|
// @ts-ignore
|
||||||
|
public constructor(public broker: ServiceBroker, schema: ServiceSchema<{}> = {}) {
|
||||||
|
super(broker);
|
||||||
|
console.log(DbMixin);
|
||||||
|
this.parseServiceSchema(
|
||||||
|
Service.mergeSchemas(
|
||||||
|
{
|
||||||
|
name: "import",
|
||||||
|
mixins: [DbMixin("comics", Comic)],
|
||||||
|
settings: {
|
||||||
|
// Available fields in the responses
|
||||||
|
fields: ["_id", "name", "quantity", "price"],
|
||||||
|
|
||||||
|
// Validator for the `create` & `insert` actions.
|
||||||
|
entityValidator: {
|
||||||
|
name: "string|min:3",
|
||||||
|
price: "number|positive",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hooks: {},
|
||||||
|
actions: {
|
||||||
|
hello: {
|
||||||
|
rest: "POST /hello",
|
||||||
|
params: {
|
||||||
|
id: "string",
|
||||||
|
},
|
||||||
|
/** @param {Context} ctx */
|
||||||
|
async handler(
|
||||||
|
ctx: Context<{ id: string; value: number }>
|
||||||
|
) {
|
||||||
|
return { koo: "loo" };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getComicCovers: {
|
||||||
|
rest: "POST /getComicCovers",
|
||||||
|
params: {
|
||||||
|
extractionOptions: "object",
|
||||||
|
walkedFolders: "array",
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async handler(
|
||||||
|
ctx: Context<{
|
||||||
|
extractionOptions: IExtractionOptions;
|
||||||
|
walkedFolders: IFolderData[];
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
console.log(ctx.params);
|
||||||
|
const foo = await getCovers(
|
||||||
|
ctx.params.extractionOptions,
|
||||||
|
ctx.params.walkedFolders
|
||||||
|
);
|
||||||
|
return foo;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
},
|
||||||
|
schema
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
test/integration/products.service.spec.ts
Normal file
113
test/integration/products.service.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { ServiceBroker } from "moleculer";
|
||||||
|
import TestService from "../../services/products.service";
|
||||||
|
|
||||||
|
describe("Test 'products' service", () => {
|
||||||
|
|
||||||
|
describe("Test actions", () => {
|
||||||
|
const broker = new ServiceBroker({ logger: false });
|
||||||
|
const service = broker.createService(TestService);
|
||||||
|
service.seedDB = null; // Disable seeding
|
||||||
|
|
||||||
|
beforeAll(() => broker.start());
|
||||||
|
afterAll(() => broker.stop());
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 999,
|
||||||
|
};
|
||||||
|
let newID: string;
|
||||||
|
|
||||||
|
it("should contains the seeded items", async () => {
|
||||||
|
const res = await broker.call("products.list");
|
||||||
|
expect(res).toEqual({ page: 1, pageSize: 10, rows: [], total: 0, totalPages: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should add the new item", async () => {
|
||||||
|
const res: any = await broker.call("products.create", record);
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 999,
|
||||||
|
quantity: 0,
|
||||||
|
});
|
||||||
|
newID = res._id;
|
||||||
|
|
||||||
|
const res2 = await broker.call("products.count");
|
||||||
|
expect(res2).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get the saved item", async () => {
|
||||||
|
const res = await broker.call("products.get", { id: newID });
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 999,
|
||||||
|
quantity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const res2 = await broker.call("products.list");
|
||||||
|
expect(res2).toEqual({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
rows: [{ _id: newID, name: "Awesome item", price: 999, quantity: 0 }],
|
||||||
|
total: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update an item", async () => {
|
||||||
|
const res = await broker.call("products.update", { id: newID, price: 499 });
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 499,
|
||||||
|
quantity: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get the updated item", async () => {
|
||||||
|
const res = await broker.call("products.get", { id: newID });
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 499,
|
||||||
|
quantity: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should increase the quantity", async () => {
|
||||||
|
const res = await broker.call("products.increaseQuantity", { id: newID, value: 5 });
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 499,
|
||||||
|
quantity: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should decrease the quantity", async () => {
|
||||||
|
const res = await broker.call("products.decreaseQuantity", { id: newID, value: 2 });
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: expect.any(String),
|
||||||
|
name: "Awesome item",
|
||||||
|
price: 499,
|
||||||
|
quantity: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the updated item", async () => {
|
||||||
|
const res = await broker.call("products.remove", { id: newID });
|
||||||
|
expect(res).toBe(1);
|
||||||
|
|
||||||
|
const res2 = await broker.call("products.count");
|
||||||
|
expect(res2).toBe(0);
|
||||||
|
|
||||||
|
const res3 = await broker.call("products.list");
|
||||||
|
expect(res3).toEqual({ page: 1, pageSize: 10, rows: [], total: 0, totalPages: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
85
test/unit/mixins/db.mixin.spec.ts
Normal file
85
test/unit/mixins/db.mixin.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { ServiceBroker } from "moleculer";
|
||||||
|
import DbService from "moleculer-db";
|
||||||
|
import DbMixin from "../../../mixins/db.mixin";
|
||||||
|
|
||||||
|
describe("Test DB mixin", () => {
|
||||||
|
|
||||||
|
describe("Test schema generator", () => {
|
||||||
|
const broker = new ServiceBroker({ logger: false, cacher: "Memory" });
|
||||||
|
|
||||||
|
beforeAll(() => broker.start());
|
||||||
|
afterAll(() => broker.stop());
|
||||||
|
|
||||||
|
it("check schema properties", async () => {
|
||||||
|
const schema = new DbMixin("my-collection").start();
|
||||||
|
|
||||||
|
expect(schema.mixins).toEqual([DbService]);
|
||||||
|
// @ts-ignore
|
||||||
|
expect(schema.adapter).toBeInstanceOf(DbService.MemoryAdapter);
|
||||||
|
expect(schema.started).toBeDefined();
|
||||||
|
expect(schema.events["cache.clean.my-collection"]).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("check cache event handler", async () => {
|
||||||
|
jest.spyOn(broker.cacher, "clean");
|
||||||
|
|
||||||
|
const schema = new DbMixin("my-collection").start();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
await schema.events["cache.clean.my-collection"].call({ broker, fullName: "my-service" });
|
||||||
|
|
||||||
|
expect(broker.cacher.clean).toBeCalledTimes(1);
|
||||||
|
expect(broker.cacher.clean).toBeCalledWith("my-service.*");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Check service started handler", () => {
|
||||||
|
|
||||||
|
it("should not call seedDB method", async () => {
|
||||||
|
const schema = new DbMixin("my-collection").start();
|
||||||
|
|
||||||
|
schema.adapter.count = jest.fn(async () => 10);
|
||||||
|
const seedDBFn = jest.fn();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
await schema.started.call({ broker, logger: broker.logger, adapter: schema.adapter, seedDB: seedDBFn });
|
||||||
|
|
||||||
|
expect(schema.adapter.count).toBeCalledTimes(1);
|
||||||
|
expect(schema.adapter.count).toBeCalledWith();
|
||||||
|
|
||||||
|
expect(seedDBFn).toBeCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call seedDB method", async () => {
|
||||||
|
const schema = new DbMixin("my-collection").start();
|
||||||
|
|
||||||
|
schema.adapter.count = jest.fn(async () => 0);
|
||||||
|
const seedDBFn = jest.fn();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
await schema.started.call({ broker, logger: broker.logger, adapter: schema.adapter, seedDB: seedDBFn });
|
||||||
|
|
||||||
|
expect(schema.adapter.count).toBeCalledTimes(2);
|
||||||
|
expect(schema.adapter.count).toBeCalledWith();
|
||||||
|
|
||||||
|
expect(seedDBFn).toBeCalledTimes(1);
|
||||||
|
expect(seedDBFn).toBeCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should broadcast a cache clear event", async () => {
|
||||||
|
const schema = new DbMixin("my-collection").start();
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
broadcast: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await schema.methods.entityChanged(null, null, ctx);
|
||||||
|
|
||||||
|
expect(ctx.broadcast).toBeCalledTimes(1);
|
||||||
|
expect(ctx.broadcast).toBeCalledWith("cache.clean.my-collection");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
40
test/unit/services/greeter.spec.ts
Normal file
40
test/unit/services/greeter.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { Errors, ServiceBroker} from "moleculer";
|
||||||
|
import TestService from "../../../services/greeter.service";
|
||||||
|
|
||||||
|
describe("Test 'greeter' service", () => {
|
||||||
|
const broker = new ServiceBroker({ logger: false });
|
||||||
|
broker.createService(TestService);
|
||||||
|
|
||||||
|
beforeAll(() => broker.start());
|
||||||
|
afterAll(() => broker.stop());
|
||||||
|
|
||||||
|
describe("Test 'greeter.hello' action", () => {
|
||||||
|
|
||||||
|
it("should return with 'Hello Moleculer'", async () => {
|
||||||
|
const res = await broker.call("greeter.hello");
|
||||||
|
expect(res).toBe("Hello Moleculer");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test 'greeter.welcome' action", () => {
|
||||||
|
|
||||||
|
it("should return with 'Welcome'", async () => {
|
||||||
|
const res = await broker.call("greeter.welcome", { name: "Adam" });
|
||||||
|
expect(res).toBe("Welcome, Adam");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject an ValidationError", async () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
try {
|
||||||
|
await broker.call("greeter.welcome");
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(Errors.ValidationError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
180
test/unit/services/products.spec.ts
Normal file
180
test/unit/services/products.spec.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { Context, Errors, ServiceBroker } from "moleculer";
|
||||||
|
import TestService from "../../../services/products.service";
|
||||||
|
|
||||||
|
describe("Test 'products' service", () => {
|
||||||
|
|
||||||
|
describe("Test actions", () => {
|
||||||
|
const broker = new ServiceBroker({ logger: false });
|
||||||
|
const service = broker.createService(TestService);
|
||||||
|
|
||||||
|
jest.spyOn(service.adapter, "updateById");
|
||||||
|
jest.spyOn(service, "transformDocuments");
|
||||||
|
jest.spyOn(service, "entityChanged");
|
||||||
|
|
||||||
|
beforeAll(() => broker.start());
|
||||||
|
afterAll(() => broker.stop());
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
_id: "123",
|
||||||
|
name: "Awesome thing",
|
||||||
|
price: 999,
|
||||||
|
quantity: 25,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Test 'products.increaseQuantity'", () => {
|
||||||
|
|
||||||
|
it("should call the adapter updateById method & transform result", async () => {
|
||||||
|
service.adapter.updateById.mockImplementation(async () => record);
|
||||||
|
service.transformDocuments.mockClear();
|
||||||
|
service.entityChanged.mockClear();
|
||||||
|
|
||||||
|
const res = await broker.call("products.increaseQuantity", {
|
||||||
|
id: "123",
|
||||||
|
value: 10,
|
||||||
|
});
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: "123",
|
||||||
|
name: "Awesome thing",
|
||||||
|
price: 999,
|
||||||
|
quantity: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.adapter.updateById).toBeCalledTimes(1);
|
||||||
|
expect(service.adapter.updateById).toBeCalledWith("123", { $inc: { quantity: 10 } } );
|
||||||
|
|
||||||
|
expect(service.transformDocuments).toBeCalledTimes(1);
|
||||||
|
expect(service.transformDocuments).toBeCalledWith(expect.any(Context), { id: "123", value: 10 }, record);
|
||||||
|
|
||||||
|
expect(service.entityChanged).toBeCalledTimes(1);
|
||||||
|
expect(service.entityChanged).toBeCalledWith("updated", { _id: "123", name: "Awesome thing", price: 999, quantity: 25 }, expect.any(Context));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test 'products.decreaseQuantity'", () => {
|
||||||
|
|
||||||
|
it("should call the adapter updateById method & transform result", async () => {
|
||||||
|
service.adapter.updateById.mockClear();
|
||||||
|
service.transformDocuments.mockClear();
|
||||||
|
service.entityChanged.mockClear();
|
||||||
|
|
||||||
|
const res = await broker.call("products.decreaseQuantity", {
|
||||||
|
id: "123",
|
||||||
|
value: 10,
|
||||||
|
});
|
||||||
|
expect(res).toEqual({
|
||||||
|
_id: "123",
|
||||||
|
name: "Awesome thing",
|
||||||
|
price: 999,
|
||||||
|
quantity: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.adapter.updateById).toBeCalledTimes(1);
|
||||||
|
expect(service.adapter.updateById).toBeCalledWith("123", { $inc: { quantity: -10 } } );
|
||||||
|
|
||||||
|
expect(service.transformDocuments).toBeCalledTimes(1);
|
||||||
|
expect(service.transformDocuments).toBeCalledWith(expect.any(Context), { id: "123", value: 10 }, record);
|
||||||
|
|
||||||
|
expect(service.entityChanged).toBeCalledTimes(1);
|
||||||
|
expect(service.entityChanged).toBeCalledWith("updated", { _id: "123", name: "Awesome thing", price: 999, quantity: 25 }, expect.any(Context));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error if params is not valid", async () => {
|
||||||
|
service.adapter.updateById.mockClear();
|
||||||
|
service.transformDocuments.mockClear();
|
||||||
|
service.entityChanged.mockClear();
|
||||||
|
|
||||||
|
expect.assertions(2);
|
||||||
|
try {
|
||||||
|
await broker.call("products.decreaseQuantity", {
|
||||||
|
id: "123",
|
||||||
|
value: -5,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).toBeInstanceOf(Errors.ValidationError);
|
||||||
|
expect(err.data).toEqual([{
|
||||||
|
action: "products.decreaseQuantity",
|
||||||
|
actual: -5,
|
||||||
|
field: "value",
|
||||||
|
message: "The 'value' field must be a positive number.",
|
||||||
|
nodeID: broker.nodeID,
|
||||||
|
type: "numberPositive",
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test methods", () => {
|
||||||
|
const broker = new ServiceBroker({ logger: false });
|
||||||
|
const service = broker.createService(TestService);
|
||||||
|
|
||||||
|
jest.spyOn(service.adapter, "insertMany");
|
||||||
|
jest.spyOn(service, "seedDB");
|
||||||
|
|
||||||
|
beforeAll(() => broker.start());
|
||||||
|
afterAll(() => broker.stop());
|
||||||
|
|
||||||
|
describe("Test 'seedDB'", () => {
|
||||||
|
|
||||||
|
it("should be called after service started & DB connected", async () => {
|
||||||
|
expect(service.seedDB).toBeCalledTimes(1);
|
||||||
|
expect(service.seedDB).toBeCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should insert 3 documents", async () => {
|
||||||
|
expect(service.adapter.insertMany).toBeCalledTimes(1);
|
||||||
|
expect(service.adapter.insertMany).toBeCalledWith([
|
||||||
|
{ name: "Samsung Galaxy S10 Plus", quantity: 10, price: 704 },
|
||||||
|
{ name: "iPhone 11 Pro", quantity: 25, price: 999 },
|
||||||
|
{ name: "Huawei P30 Pro", quantity: 15, price: 679 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test hooks", () => {
|
||||||
|
const broker = new ServiceBroker({ logger: false });
|
||||||
|
const createActionFn = jest.fn();
|
||||||
|
// @ts-ignore
|
||||||
|
broker.createService(TestService, {
|
||||||
|
actions: {
|
||||||
|
create: {
|
||||||
|
handler: createActionFn,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(() => broker.start());
|
||||||
|
afterAll(() => broker.stop());
|
||||||
|
|
||||||
|
describe("Test before 'create' hook", () => {
|
||||||
|
|
||||||
|
it("should add quantity with zero", async () => {
|
||||||
|
await broker.call("products.create", {
|
||||||
|
id: "111",
|
||||||
|
name: "Test product",
|
||||||
|
price: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createActionFn).toBeCalledTimes(1);
|
||||||
|
expect(createActionFn.mock.calls[0][0].params).toEqual({
|
||||||
|
id: "111",
|
||||||
|
name: "Test product",
|
||||||
|
price: 100,
|
||||||
|
quantity: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"removeComments": true,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"pretty": true,
|
||||||
|
"target": "es6",
|
||||||
|
"outDir": "dist",
|
||||||
|
},
|
||||||
|
"include": ["./**/*"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/**/*",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
}
|
||||||
20
utils/logger.utils.ts
Normal file
20
utils/logger.utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { default as Pino } from "pino";
|
||||||
|
import { default as pinopretty } from "pino-pretty";
|
||||||
|
|
||||||
|
export const logger = Pino({
|
||||||
|
name: "threetwo!",
|
||||||
|
prettyPrint: { colorize: true },
|
||||||
|
// crlf: false,
|
||||||
|
// errorLikeObjectKeys: ["err", "error"],
|
||||||
|
// errorProps: "",
|
||||||
|
// levelFirst: false, // --levelFirst
|
||||||
|
messageKey: "msg", // --messageKey
|
||||||
|
levelKey: "level", // --levelKey
|
||||||
|
// messageFormat: false, // --messageFormat
|
||||||
|
// timestampKey: "time", // --timestampKey
|
||||||
|
// translateTime: false, // --translateTime
|
||||||
|
// search: "foo == `bar`", // --search
|
||||||
|
// ignore: "pid,hostname", // --ignore
|
||||||
|
// hideObject: false, // --hideObject
|
||||||
|
// singleLine: false,
|
||||||
|
});
|
||||||
323
utils/uncompression.utils.ts
Normal file
323
utils/uncompression.utils.ts
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021 Rishi Ghan
|
||||||
|
*
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Revision History:
|
||||||
|
* Initial: 2021/05/04 Rishi Ghan
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createReadStream, createWriteStream } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { default as unzipper } from "unzipper";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { each, isEmpty, map, remove, indexOf } from "lodash";
|
||||||
|
import {
|
||||||
|
IExplodedPathResponse,
|
||||||
|
IExtractComicBookCoverErrorResponse,
|
||||||
|
IExtractedComicBookCoverFile,
|
||||||
|
IExtractionOptions,
|
||||||
|
IFolderData,
|
||||||
|
} from "../interfaces/folder.interface";
|
||||||
|
import { logger } from "./logger.utils";
|
||||||
|
const { writeFile, readFile } = require("fs").promises;
|
||||||
|
const sharp = require("sharp");
|
||||||
|
const unrarer = require("node-unrar-js");
|
||||||
|
const Walk = require("@root/walk");
|
||||||
|
const fse = require("fs-extra");
|
||||||
|
|
||||||
|
|
||||||
|
export const unrar = async (
|
||||||
|
extractionOptions: IExtractionOptions,
|
||||||
|
walkedFolder: IFolderData,
|
||||||
|
): Promise<
|
||||||
|
| IExtractedComicBookCoverFile
|
||||||
|
| IExtractedComicBookCoverFile[]
|
||||||
|
| IExtractComicBookCoverErrorResponse
|
||||||
|
> => {
|
||||||
|
const paths = constructPaths(extractionOptions, walkedFolder);
|
||||||
|
const directoryOptions = {
|
||||||
|
mode: 0o2775,
|
||||||
|
};
|
||||||
|
const fileBuffer = await readFile(paths.inputFilePath).catch(err =>
|
||||||
|
console.error("Failed to read file", err),
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
await fse.ensureDir(paths.targetPath, directoryOptions);
|
||||||
|
logger.info(`${paths.targetPath} was created.`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${error}: Couldn't create directory.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractor = await unrarer.createExtractorFromData({ data: fileBuffer });
|
||||||
|
|
||||||
|
switch (extractionOptions.extractTarget) {
|
||||||
|
case "cover":
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let fileNameToExtract = "";
|
||||||
|
const list = extractor.getFileList();
|
||||||
|
const fileHeaders = [...list.fileHeaders];
|
||||||
|
each(fileHeaders, async fileHeader => {
|
||||||
|
const fileName = explodePath(fileHeader.name).fileName;
|
||||||
|
if (
|
||||||
|
fileName !== "" &&
|
||||||
|
fileHeader.flags.directory === false &&
|
||||||
|
isEmpty(fileNameToExtract)
|
||||||
|
) {
|
||||||
|
logger.info(`Attempting to write ${fileHeader.name}`);
|
||||||
|
fileNameToExtract = fileHeader.name;
|
||||||
|
const file = extractor.extract({ files: [fileHeader.name] });
|
||||||
|
const extractedFile = [...file.files][0];
|
||||||
|
const fileArrayBuffer = extractedFile.extraction;
|
||||||
|
await writeFile(
|
||||||
|
paths.targetPath + "/" + fileName,
|
||||||
|
fileArrayBuffer,
|
||||||
|
);
|
||||||
|
resolve({
|
||||||
|
name: `${fileName}`,
|
||||||
|
path: paths.targetPath,
|
||||||
|
fileSize: fileHeader.packSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${error}: Couldn't write file.`);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case "all":
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const files = extractor.extract({});
|
||||||
|
const extractedFiles = [...files.files];
|
||||||
|
const comicBookCoverFiles: IExtractedComicBookCoverFile[] = [];
|
||||||
|
for (const file of extractedFiles) {
|
||||||
|
logger.info(`Attempting to write ${file.fileHeader.name}`);
|
||||||
|
const fileBuffer = file.extraction;
|
||||||
|
const fileName = explodePath(file.fileHeader.name).fileName;
|
||||||
|
|
||||||
|
if (fileName !== "" && file.fileHeader.flags.directory === false) {
|
||||||
|
await writeFile(paths.targetPath + "/" + fileName, fileBuffer);
|
||||||
|
}
|
||||||
|
comicBookCoverFiles.push({
|
||||||
|
name: `${file.fileHeader.name}`,
|
||||||
|
path: paths.targetPath,
|
||||||
|
fileSize: file.fileHeader.packSize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolve(_.flatten(comicBookCoverFiles));
|
||||||
|
} catch (error) {
|
||||||
|
resolve({
|
||||||
|
message: `${error}`,
|
||||||
|
errorCode: "500",
|
||||||
|
data: walkedFolder.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
message: "File format not supported, yet.",
|
||||||
|
errorCode: "90",
|
||||||
|
data: "asda",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unzip = async (
|
||||||
|
extractionOptions: IExtractionOptions,
|
||||||
|
walkedFolder: IFolderData,
|
||||||
|
): Promise<
|
||||||
|
| IExtractedComicBookCoverFile[]
|
||||||
|
| IExtractedComicBookCoverFile
|
||||||
|
| IExtractComicBookCoverErrorResponse
|
||||||
|
> => {
|
||||||
|
const directoryOptions = {
|
||||||
|
mode: 0o2775,
|
||||||
|
};
|
||||||
|
const paths = constructPaths(extractionOptions, walkedFolder);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fse.ensureDir(paths.targetPath, directoryOptions);
|
||||||
|
logger.info(`${paths.targetPath} was created or already exists.`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${error} Couldn't create directory.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedFiles: IExtractedComicBookCoverFile[] = [];
|
||||||
|
const zip = createReadStream(paths.inputFilePath).pipe(
|
||||||
|
unzipper.Parse({ forceStream: true }),
|
||||||
|
);
|
||||||
|
for await (const entry of zip) {
|
||||||
|
const fileName = explodePath(entry.path).fileName;
|
||||||
|
const size = entry.vars.uncompressedSize;
|
||||||
|
if (
|
||||||
|
extractedFiles.length === 1 &&
|
||||||
|
extractionOptions.extractTarget === "cover"
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (fileName !== "" && entry.type !== "Directory") {
|
||||||
|
logger.info(`Attempting to write ${fileName}`);
|
||||||
|
entry.pipe(createWriteStream(paths.targetPath + "/" + fileName));
|
||||||
|
extractedFiles.push({
|
||||||
|
name: fileName,
|
||||||
|
fileSize: size,
|
||||||
|
path: paths.targetPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
entry.autodrain();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
logger.info("");
|
||||||
|
resolve(_.flatten(extractedFiles));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractArchive = async (
|
||||||
|
extractionOptions: IExtractionOptions,
|
||||||
|
walkedFolder: IFolderData,
|
||||||
|
): Promise<
|
||||||
|
| IExtractedComicBookCoverFile
|
||||||
|
| IExtractedComicBookCoverFile[]
|
||||||
|
| IExtractComicBookCoverErrorResponse
|
||||||
|
> => {
|
||||||
|
switch (walkedFolder.extension) {
|
||||||
|
case ".cbz":
|
||||||
|
return await unzip(extractionOptions, walkedFolder);
|
||||||
|
case ".cbr":
|
||||||
|
return await unrar(extractionOptions, walkedFolder);
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
message: "File format not supported, yet.",
|
||||||
|
errorCode: "90",
|
||||||
|
data: `${extractionOptions}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCovers = async (
|
||||||
|
options: IExtractionOptions,
|
||||||
|
walkedFolders: IFolderData[],
|
||||||
|
): Promise<
|
||||||
|
| IExtractedComicBookCoverFile
|
||||||
|
| IExtractComicBookCoverErrorResponse
|
||||||
|
| IExtractedComicBookCoverFile[]
|
||||||
|
| (
|
||||||
|
| IExtractedComicBookCoverFile
|
||||||
|
| IExtractComicBookCoverErrorResponse
|
||||||
|
| IExtractedComicBookCoverFile[]
|
||||||
|
)[]
|
||||||
|
| IExtractComicBookCoverErrorResponse
|
||||||
|
> => {
|
||||||
|
switch (options.extractionMode) {
|
||||||
|
case "bulk":
|
||||||
|
const extractedDataPromises = map(walkedFolders, async folder => await extractArchive(options, folder));
|
||||||
|
return Promise.all(extractedDataPromises).then(data => _.flatten(data));
|
||||||
|
case "single":
|
||||||
|
return await extractArchive(options, walkedFolders[0]);
|
||||||
|
default:
|
||||||
|
logger.error("Unknown extraction mode selected.");
|
||||||
|
return {
|
||||||
|
message: "Unknown extraction mode selected.",
|
||||||
|
errorCode: "90",
|
||||||
|
data: `${options}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const walkFolder = async (folder: string): Promise<IFolderData[]> => {
|
||||||
|
const result: IFolderData[] = [];
|
||||||
|
let walkResult: IFolderData = {
|
||||||
|
name: "",
|
||||||
|
extension: "",
|
||||||
|
containedIn: "",
|
||||||
|
isFile: false,
|
||||||
|
isLink: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const walk = Walk.create({ sort: filterOutDotFiles });
|
||||||
|
await walk(folder, async (err, pathname, dirent) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error("Failed to lstat directory", { error: err });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ([".cbz", ".cbr"].includes(path.extname(dirent.name))) {
|
||||||
|
walkResult = {
|
||||||
|
name: path.basename(dirent.name, path.extname(dirent.name)),
|
||||||
|
extension: path.extname(dirent.name),
|
||||||
|
containedIn: path.dirname(pathname),
|
||||||
|
isFile: dirent.isFile(),
|
||||||
|
isLink: dirent.isSymbolicLink(),
|
||||||
|
};
|
||||||
|
logger.info(
|
||||||
|
`Scanned ${dirent.name} contained in ${path.dirname(pathname)}`,
|
||||||
|
);
|
||||||
|
result.push(walkResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const explodePath = (filePath: string): IExplodedPathResponse => {
|
||||||
|
const exploded = filePath.split("/");
|
||||||
|
const fileName = remove(exploded, item => indexOf(exploded, item) === exploded.length - 1).join("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
exploded,
|
||||||
|
fileName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const constructPaths = (
|
||||||
|
extractionOptions: IExtractionOptions,
|
||||||
|
walkedFolder: IFolderData,
|
||||||
|
) => ({
|
||||||
|
targetPath:
|
||||||
|
extractionOptions.targetExtractionFolder + "/" + walkedFolder.name,
|
||||||
|
inputFilePath:
|
||||||
|
walkedFolder.containedIn +
|
||||||
|
"/" +
|
||||||
|
walkedFolder.name +
|
||||||
|
walkedFolder.extension,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const extractMetadataFromImage = async (
|
||||||
|
imageFilePath: string,
|
||||||
|
): Promise<unknown> => {
|
||||||
|
const image = await sharp(imageFilePath)
|
||||||
|
.metadata()
|
||||||
|
.then(function(metadata) {
|
||||||
|
return metadata;
|
||||||
|
});
|
||||||
|
return image;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOutDotFiles = entities => entities.filter(ent => !ent.name.startsWith("."));
|
||||||
Reference in New Issue
Block a user