🍔 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