diff --git a/.codeclimate.yml b/.codeclimate.yml index 7bd2045f9..31c8d76d7 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -10,6 +10,7 @@ plugins: count_threshold: 3 languages: typescript: + mass_threshold: 45 javascript: python: python_version: 3 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..d74ca64c3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,108 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Digital Twin as a Service", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + "features": { + "ghcr.io/devcontainers-contrib/features/apt-get-packages:1": { + "clean_ppas": true, + "preserve_apt_list": true, + "packages": "curl graphviz htop net-tools powerline" + }, + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": true, + "configureZshAsDefaultShell": true, + "installOhMyZsh": true, + "installOhMyZshConfig": true, + "username": "devcontainer", + "userUid": "1001", + "userGid": "automatic" + }, + "ghcr.io/devcontainers-contrib/features/exa:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "os-provided" + }, + "ghcr.io/devcontainers-contrib/features/markdownlint-cli:1": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/mkdocs:2": { + "version": "latest", + "plugins": "mkdocs-material pymdown-extensions mkdocstrings[crystal,python] mkdocs-monorepo-plugin mkdocs-pdf-export-plugin mkdocs-awesome-pages-plugin python-markdown-math mkdocs-open-in-new-tab mkdocs-with-pdf qrcode" + }, + "ghcr.io/devcontainers-contrib/features/nestjs-cli:2": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/npm-package:1": { + "package": "madge" + }, + "ghcr.io/devcontainers-contrib/features/pipx-package:1": {}, + "ghcr.io/devcontainers-contrib/features/poetry:2": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/pre-commit:2": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/tmux-apt-get:1": {}, + "ghcr.io/devcontainers-contrib/features/typescript:2": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/vercel-serve:1": { + "version": "latest" + }, + "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": { + "plugins": "ssh-agent npm zsh-autosuggestions", + "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions", + "username": "node" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "AlexShen.classdiagram-ts", + "christian-kohler.npm-intellisense", + "DavidAnson.vscode-markdownlin", + "donjayamanne.githistory", + "eamodio.gitlens", + "humao.rest-client", + "johnpapa.vscode-peacock", + "mhutchie.git-graph", + "SimonSiefke.svg-preview", + "redhat.vscode-yaml", + "VisualStudioExptTeam.vscodeintellicode", + "vscode-icons-team.vscode-icons" + ], + "settings": { + "editor.tabSize": 2, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "bash", + "icon": "terminal-bash" + }, + "zsh": { + "path": "zsh" + }, + "tmux": { + "path": "tmux", + "icon": "terminal-tmux" + }, + "pwsh": { + "path": "pwsh", + "icon": "terminal-powershell" + } + } + } + } + }, + "forwardPorts": [ + 4000, //react client + 4001, // lib microservice + 5000, // DT runner + 8000 // mkdocs + ] +} +// Execute after login: +// source /usr/share/powerline/bindings/zsh/powerline.zsh diff --git a/servers/execution/runner/DEVELOPER.md b/servers/execution/runner/DEVELOPER.md index deea65472..5fbb7f732 100644 --- a/servers/execution/runner/DEVELOPER.md +++ b/servers/execution/runner/DEVELOPER.md @@ -18,6 +18,12 @@ yarn start # Start the application yarn clean # Deletes directories "build", "coverage", and "dist" ``` +### On Filenames in Tests + +The jest and nestjs combination can not detect tests in files +with _config_ in their names. Hence, the config word has been +replaced with _options_ in the names of test files. + ## :package: :ship: NPM package ### Github Package Registry diff --git a/servers/execution/runner/README.md b/servers/execution/runner/README.md index 0d989a0a8..6702b24b1 100644 --- a/servers/execution/runner/README.md +++ b/servers/execution/runner/README.md @@ -35,7 +35,7 @@ The template configuration file is: ```ini port: 5000 -location: "lifecycle" #directory location of scripts +location: "script" #directory location of scripts ``` The file should be named as _runner.yaml_ and placed in the directory diff --git a/servers/execution/runner/jest.config.json b/servers/execution/runner/jest.config.json index d6016b6b0..d507a0b43 100644 --- a/servers/execution/runner/jest.config.json +++ b/servers/execution/runner/jest.config.json @@ -16,8 +16,7 @@ "node_modules", "./dist", "../src/app.module.ts", - "../src/main.ts", - "../src/runner.ts" + "../src/main.ts" ], "extensionsToTreatAsEsm": [".ts"], "moduleFileExtensions": [ @@ -25,7 +24,7 @@ "json", "ts" ], - "modulePathIgnorePatterns": ["config"], + "modulePathIgnorePatterns": [], "coverageDirectory": "/coverage/", "coverageThreshold": { "global": { diff --git a/servers/execution/runner/package.json b/servers/execution/runner/package.json index 08b5a6c15..18ce7baa1 100644 --- a/servers/execution/runner/package.json +++ b/servers/execution/runner/package.json @@ -1,6 +1,6 @@ { "name": "@into-cps-association/runner", - "version": "0.1.1", + "version": "0.2.0", "description": "DT Runner", "main": "dist/src/runner.js", "repository": "https://github.com/into-cps-association/DTaaS.git", @@ -11,20 +11,20 @@ "scripts": { "build": "npx tsc", "clean": "npx rimraf build node_modules coverage dist src.svg test.svg", + "check:final": "concurrently -s all -g -n syntax,format,graph,build,test \"yarn syntax\" \"yarn format\" \"yarn graph\" \"yarn build\" \"yarn test\"", "format": "prettier --ignore-path ../.gitignore --write \"**/*.{ts,tsx,css,scss}\"", "start": "npx cross-env NODE_OPTIONS='--es-module-specifier-resolution=node --experimental-specifier-resolution=node' NODE_NO_WARNINGS=1 node dist/src/main.js", "syntax": "npx eslint . --fix", - "pretest": "npx rimraf runner.yaml scripts && npx shx cp test/config/runner.yaml runner.yaml && npx shx mkdir scripts && npx shx cp test/data/scripts/* scripts/.", - "posttest": "npx rimraf runner.yaml scripts", + "pretest": "npx rimraf runner.test.yaml script && npx shx cp test/config/runner.test.yaml runner.test.yaml && npx shx mkdir script && npx shx cp test/data/script/* script/.", + "posttest": "npx rimraf runner.test.yaml script", "test": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true", - "test:e2e": "yarn pretest && npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true --verbose=true test/app.e2e.spec.ts && yarn posttest", - "test:nocov": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=false", - "test:watchAll": "npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true --watchAll", + "test:e2e": "yarn pretest && npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true --verbose=true test/e2e && yarn posttest", + "test:int": "yarn pretest && npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true --verbose=true test/integration && yarn posttest", + "test:nocov": "yarn pretest && npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=false && yarn posttest", + "test:unit": "yarn pretest && npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true --verbose=true test/unit && yarn posttest", + "test:watchAll": "yarn pretest && npx cross-env NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 jest --coverage=true --watchAll && yarn posttest", "graph": "npx madge --image src.svg src && npx madge --image test.svg test" }, - "script-unused": { - "runner": "npx cross-env NODE_OPTIONS='--es-module-specifier-resolution=node --experimental-modules --experimental-specifier-resolution=node' NODE_NO_WARNINGS=1 node dist/src/runner.js" - }, "bin": { "runner": "./dist/src/main.js" }, @@ -48,6 +48,7 @@ "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.6.0", "@typescript-eslint/parser": "^7.6.0", + "concurrently": "^8.2.2", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^9.1.0", @@ -69,12 +70,16 @@ "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.3.7", "@nestjs/platform-express": "^10.3.7", + "chalk": "^5.3.0", + "commander": "^12.0.0", "cross-env": "^7.0.3", "execa": "^8.0.1", "express": "^4.19.2", "js-yaml": "^4.1.0", + "keyv": "^4.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "zod": "^3.22.4" - } + }, + "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" } diff --git a/servers/execution/runner/runner.yaml b/servers/execution/runner/runner.yaml index 9180529a1..d3c4a6953 100644 --- a/servers/execution/runner/runner.yaml +++ b/servers/execution/runner/runner.yaml @@ -1,2 +1,3 @@ port: 5000 -location: "scripts" #directory location of scripts +location: 'script' #directory location of scripts +commands: [] \ No newline at end of file diff --git a/servers/execution/runner/runner.yaml.sample b/servers/execution/runner/runner.yaml.sample index 9180529a1..fa0b07b68 100644 --- a/servers/execution/runner/runner.yaml.sample +++ b/servers/execution/runner/runner.yaml.sample @@ -1,2 +1,6 @@ port: 5000 -location: "scripts" #directory location of scripts +location: 'script' #directory location of scripts +commands: + - create + - execute + - terminate \ No newline at end of file diff --git a/servers/execution/runner/src/app.controller.ts b/servers/execution/runner/src/app.controller.ts index b52b68190..70f157bc4 100644 --- a/servers/execution/runner/src/app.controller.ts +++ b/servers/execution/runner/src/app.controller.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import { Response } from 'express'; import { CommandStatus } from './interfaces/command.interface.js'; -import ExecaManager from './execaManager.service.js'; +import ExecaManager from './execa-manager.service.js'; import { ExecuteCommandDto, executeCommandSchema } from './dto/command.dto.js'; import ZodValidationPipe from './validation.pipe.js'; @@ -19,13 +19,13 @@ export default class AppController { constructor(private readonly manager: ExecaManager) {} // eslint-disable-line no-empty-function @Get('history') - getHello(): Array { + getHistory(): Array { return this.manager.checkHistory(); } @Post() @UsePipes(new ZodValidationPipe(executeCommandSchema)) - async changePhase( + async newCommand( @Body() executeCommandDto: ExecuteCommandDto, @Res({ passthrough: true }) res: Response, ): Promise { diff --git a/servers/execution/runner/src/app.module.ts b/servers/execution/runner/src/app.module.ts index 3e3de005c..51dd45285 100644 --- a/servers/execution/runner/src/app.module.ts +++ b/servers/execution/runner/src/app.module.ts @@ -1,18 +1,13 @@ import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; +import RunnerFactory from './runner-factory.service.js'; import AppController from './app.controller.js'; -import ExecaManager from './execaManager.service.js'; +import ExecaManager from './execa-manager.service.js'; import Queue from './queue.service.js'; -import configuration from './config/configuration.js'; +import Config from './config/configuration.service.js'; @Module({ - imports: [ - ConfigModule.forRoot({ - ignoreEnvFile: true, - load: [configuration], - }), - ], + imports: [], controllers: [AppController], - providers: [ExecaManager, Queue], + providers: [ExecaManager, Queue, RunnerFactory, Config], }) export default class AppModule {} diff --git a/servers/execution/runner/src/config/Config.interface.ts b/servers/execution/runner/src/config/Config.interface.ts deleted file mode 100644 index 2a812f253..000000000 --- a/servers/execution/runner/src/config/Config.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Command = { - name: string; - executable: string; -}; - -export default interface Config { - port: number; - location: string; - commands: Array; -} diff --git a/servers/execution/runner/src/config/commander.ts b/servers/execution/runner/src/config/commander.ts new file mode 100644 index 000000000..cce4304d6 --- /dev/null +++ b/servers/execution/runner/src/config/commander.ts @@ -0,0 +1,42 @@ +import { Command } from 'commander'; +import { readFileSync } from 'fs'; +import chalk from 'chalk'; +import Keyv from 'keyv'; +import resolveFile from './util.js'; + +export function createCommand(name: string): [Command, Keyv] { + return [new Command(name), new Keyv()]; +} + +export default async function CLI( + program: Command, + CLIOptions: Keyv, +): Promise { + program + .description('Remote code execution for humans') + .option( + '-c --config ', + 'runner config file specified in yaml format', + 'runner.yaml', + ) + .helpOption('-h --help', 'display help') + .showHelpAfterError() + .helpInformation(); + + program.parse(process.argv); + const options = program.opts(); + + if (options.config !== undefined) { + const configFile: string = options.config; + const resolvedFilename: string = resolveFile(configFile.toString()); + try { + readFileSync(resolvedFilename, 'utf8'); + } catch (err) { + // eslint-disable-next-line no-console + console.log(chalk.bold.redBright('Config file can not be read. Exiting')); + throw new Error('Invalid configuration'); + } + await CLIOptions.set('configFile', resolvedFilename); + } + return CLIOptions; +} diff --git a/servers/execution/runner/src/config/config.interface.ts b/servers/execution/runner/src/config/config.interface.ts new file mode 100644 index 000000000..f3852dc19 --- /dev/null +++ b/servers/execution/runner/src/config/config.interface.ts @@ -0,0 +1,20 @@ +// unused type +export type PermitCommand = { + name: string; + executable: string; +}; + +export type ConfigValues = { + port: number; + // os-compatible relative filepath of the config yaml file. + // The relative filepath is with reference to the execution + // directory of the runner + location: string; + commands: Array; +}; + +export const configDefault: ConfigValues = { + port: 5000, + location: 'script', + commands: [], +}; diff --git a/servers/execution/runner/src/config/configuration.service.ts b/servers/execution/runner/src/config/configuration.service.ts new file mode 100644 index 000000000..76ac39d48 --- /dev/null +++ b/servers/execution/runner/src/config/configuration.service.ts @@ -0,0 +1,35 @@ +import { readFileSync } from 'fs'; +import path from 'node:path'; +import * as yaml from 'js-yaml'; +import { Injectable } from '@nestjs/common'; +import Keyv from 'keyv'; +import { configDefault, ConfigValues } from './config.interface.js'; +import resolveFile from './util.js'; + +@Injectable() +export default class Config { + private configValues: ConfigValues = configDefault; + + async loadConfig(CLIOptions: Keyv): Promise { + const configFile = await CLIOptions.get('configFile'); + if (configFile !== undefined) { + this.configValues = yaml.load( + readFileSync(resolveFile(configFile.toString()), 'utf8'), + ) as ConfigValues; + } + const dir = path.dirname(resolveFile(configFile.toString())); + this.configValues.location = path.join(dir, this.configValues.location); + } + + permitCommands(): Array { + return this.configValues.commands; + } + + getPort(): number { + return this.configValues.port; + } + + getLocation(): string { + return this.configValues.location; + } +} diff --git a/servers/execution/runner/src/config/configuration.ts b/servers/execution/runner/src/config/configuration.ts deleted file mode 100644 index ceb550833..000000000 --- a/servers/execution/runner/src/config/configuration.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { readFileSync } from 'fs'; -import * as yaml from 'js-yaml'; -import { join } from 'path'; -import Config from './Config.interface.js'; - -const YAML_CONFIG_FILENAME = 'runner.yaml'; - -export default function readConfig(): Config { - return yaml.load( - readFileSync(join(process.cwd(), YAML_CONFIG_FILENAME), 'utf8'), - ) as Config; -} diff --git a/servers/execution/runner/src/config/util.ts b/servers/execution/runner/src/config/util.ts new file mode 100644 index 000000000..54a974838 --- /dev/null +++ b/servers/execution/runner/src/config/util.ts @@ -0,0 +1,5 @@ +import path from 'node:path'; + +export default function resolveFile(name: string): string { + return path.resolve(name); +} diff --git a/servers/execution/runner/src/execaManager.service.ts b/servers/execution/runner/src/execa-manager.service.ts similarity index 71% rename from servers/execution/runner/src/execaManager.service.ts rename to servers/execution/runner/src/execa-manager.service.ts index db811c663..207b5a4b1 100644 --- a/servers/execution/runner/src/execaManager.service.ts +++ b/servers/execution/runner/src/execa-manager.service.ts @@ -1,31 +1,30 @@ import { join } from 'path'; +import { Injectable } from '@nestjs/common'; import { Command, Manager, CommandStatus, } from './interfaces/command.interface.js'; -import ExecaRunner from './execaRunner.js'; import Queue from './queue.service.js'; -import readConfig from './config/configuration.js'; -import Config from './config/Config.interface.js'; import { ExecuteCommandDto } from './dto/command.dto.js'; +import RunnerFactory from './runner-factory.service.js'; +import Config from './config/configuration.service.js'; -const config: Config = readConfig(); - +@Injectable() export default class ExecaManager implements Manager { - private commandQueue: Queue = new Queue(); + // eslint-disable-next-line no-useless-constructor + constructor( + private commandQueue: Queue, + private config: Config, + ) {} // eslint-disable-line no-empty-function async newCommand(name: string): Promise<[boolean, Map]> { + let success: boolean = false; const command: Command = { name, status: 'invalid', - task: new ExecaRunner(''), - // task attribute is deliberately left empty + task: RunnerFactory.create(join(this.config.getLocation(), name)), }; - - let success: boolean = false; - - command.task = new ExecaRunner(join(process.cwd(), config.location, name)); this.commandQueue.enqueue(command); await command.task.run().then((value) => { success = value; @@ -36,11 +35,8 @@ export default class ExecaManager implements Manager { checkStatus(): CommandStatus { let commandStatus: CommandStatus; - const logs: Map = new Map(); const command: Command | undefined = this.commandQueue.activeCommand(); - logs.set('stdout', ''); - logs.set('stderr', ''); if (command === undefined) { commandStatus = { name: 'none', @@ -59,7 +55,6 @@ export default class ExecaManager implements Manager { stderr: command.task.checkLogs().get('stderr'), }, }; - // console.log(command.task.checkLogs()); } return commandStatus; } diff --git a/servers/execution/runner/src/execaRunner.ts b/servers/execution/runner/src/execa-runner.ts similarity index 100% rename from servers/execution/runner/src/execaRunner.ts rename to servers/execution/runner/src/execa-runner.ts diff --git a/servers/execution/runner/src/main.ts b/servers/execution/runner/src/main.ts index 3573a0d4a..8f0decb6a 100644 --- a/servers/execution/runner/src/main.ts +++ b/servers/execution/runner/src/main.ts @@ -1,9 +1,10 @@ #!/usr/bin/env -S NODE_OPTIONS="--es-module-specifier-resolution=node --experimental-modules --experimental-specifier-resolution=node" NODE_NO_WARNINGS=1 node import { NestFactory } from '@nestjs/core'; +import Keyv from 'keyv'; import AppModule from './app.module.js'; -import Config from './config/Config.interface.js'; -import readConfig from './config/configuration.js'; +import Config from './config/configuration.service.js'; +import CLI, { createCommand } from './config/commander.js'; /* The js file extension in import is a limitation of typescript. @@ -11,10 +12,14 @@ See: https://stackoverflow.com/questions/62619058/appending-js-extension-on-rela https://github.com/microsoft/TypeScript/issues/16577 */ -const config: Config = readConfig(); +const PROGRAM_NAME = 'runner'; +const [program, CLIOptions] = createCommand(PROGRAM_NAME); +await CLI(program, CLIOptions); // function fills the CLIOptions -async function bootstrap() { +async function bootstrap(options: Keyv) { const app = await NestFactory.create(AppModule); - await app.listen(config.port); + const config = app.get(Config); + await config.loadConfig(options); + await app.listen(config.getPort()); } -bootstrap(); +bootstrap(CLIOptions); diff --git a/servers/execution/runner/src/queue.service.ts b/servers/execution/runner/src/queue.service.ts index 7a85f11d0..27ef71629 100644 --- a/servers/execution/runner/src/queue.service.ts +++ b/servers/execution/runner/src/queue.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Scope } from '@nestjs/common'; import { Command } from './interfaces/command.interface.js'; import { ExecuteCommandDto } from './dto/command.dto.js'; -@Injectable() +@Injectable({ scope: Scope.DEFAULT }) export default class Queue { private queue: Command[] = []; diff --git a/servers/execution/runner/src/runner-factory.service.ts b/servers/execution/runner/src/runner-factory.service.ts new file mode 100644 index 000000000..e1c948481 --- /dev/null +++ b/servers/execution/runner/src/runner-factory.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import Runner from './interfaces/runner.interface.js'; +import ExecaRunner from './execa-runner.js'; + +@Injectable() +export default class RunnerFactory { + static create(command: string): Runner { + return new ExecaRunner(command); + } +} diff --git a/servers/execution/runner/test/app.e2e.spec.ts b/servers/execution/runner/test/app.e2e.spec.ts deleted file mode 100644 index 4dfb80faf..000000000 --- a/servers/execution/runner/test/app.e2e.spec.ts +++ /dev/null @@ -1,181 +0,0 @@ -import supertest from 'supertest'; -import { Test } from '@nestjs/testing'; -import { INestApplication } from '@nestjs/common'; -import AppModule from 'src/app.module'; - -describe('Runner end-to-end tests', () => { - interface RequestBody { - name?: string; - command?: string; - } - interface ResponseBody { - message?: string; - error?: string; - statusCode?: number; - status?: string; - name?: string; - logs?: { stdout: string; stderr: string }; - } - - function postRequest( - route: string, - HttpStatus: number, - reqBody: RequestBody, - resBody: ResponseBody, - ) { - return supertest(app.getHttpServer()) - .post(route) - .send(reqBody) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect(HttpStatus) - .expect(resBody); - } - - function getRequest( - route: string, - HttpStatus: number, - reqBody: RequestBody, - resBody: ResponseBody | RequestBody[], - ) { - return supertest(app.getHttpServer()) - .get(route) - .send(reqBody) - .set('Content-Type', 'application/json') - .set('Accept', 'application/json') - .expect(HttpStatus) - .expect(resBody); - } - - const queriesJSON = { - valid: { - reqBody: { - name: 'create', - }, - HttpStatus: 200, - resBody: { - POST: { - status: 'success', - }, - GET: { - name: 'create', - status: 'valid', - logs: { stdout: 'hello world', stderr: '' }, - }, - }, - }, - invalid: { - reqBody: { - name: 'configure', - }, - HttpStatus: 400, - resBody: { - POST: { - status: 'invalid command', - }, - GET: { - name: 'configure', - status: 'invalid', - logs: { stdout: '', stderr: '' }, - }, - }, - }, - incorrect: { - reqBody: { - command: 'create', - }, - HttpStatus: 400, - resBody: { - POST: { - message: 'Validation Failed', - error: 'Bad Request', - statusCode: 400, - }, - GET: { - name: 'none', - status: 'invalid', - logs: { stdout: '', stderr: '' }, - }, - }, - }, - }; - - let app: INestApplication; - beforeEach(async () => { - const moduleFixture = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleFixture.createNestApplication(); - await app.init(); - }); - - afterEach(async () => { - await app.close(); - }); - - describe('POST /', () => { - const keys: (keyof typeof queriesJSON)[] = [ - 'valid', - 'invalid', - 'incorrect', - ]; - keys.forEach((key) => { - const query = queriesJSON[key]; - it(`execute ${key} command`, () => - postRequest('/', query.HttpStatus, query.reqBody, query.resBody.POST)); - }); - }); - - describe('GET /', () => { - const keys: (keyof typeof queriesJSON)[] = [ - 'valid', - 'invalid', - 'incorrect', - ]; - keys.forEach((key) => { - const query = queriesJSON[key]; - it(`execution status of ${key} command`, async () => { - await postRequest( - '/', - query.HttpStatus, - query.reqBody, - query.resBody.POST, - ); - return getRequest('/', 200, {}, query.resBody.GET); - }); - }); - - it('execution status without any prior command executions', () => - getRequest( - '/', - 200, - {}, - { name: 'none', status: 'invalid', logs: { stdout: '', stderr: '' } }, - )); - }); - - describe('GET /history', () => { - it('without any prior command executions', () => - getRequest('/history', 200, {}, [])); - - it('after two valid command executions', async () => { - await postRequest( - '/', - queriesJSON.valid.HttpStatus, - queriesJSON.valid.reqBody, - queriesJSON.valid.resBody.POST, - ); - await postRequest( - '/', - queriesJSON.valid.HttpStatus, - queriesJSON.valid.reqBody, - queriesJSON.valid.resBody.POST, - ); - return getRequest('/history', 200, {}, [ - { name: 'create' }, - { name: 'create' }, - ]); - }); - }); -}); diff --git a/servers/execution/runner/test/config/runner.test.yaml b/servers/execution/runner/test/config/runner.test.yaml new file mode 100644 index 000000000..21e536b24 --- /dev/null +++ b/servers/execution/runner/test/config/runner.test.yaml @@ -0,0 +1,4 @@ +port: 5002 +location: 'script' #directory location of scripts +commands: + - create \ No newline at end of file diff --git a/servers/execution/runner/test/config/runner.yaml b/servers/execution/runner/test/config/runner.yaml deleted file mode 100644 index 9180529a1..000000000 --- a/servers/execution/runner/test/config/runner.yaml +++ /dev/null @@ -1,2 +0,0 @@ -port: 5000 -location: "scripts" #directory location of scripts diff --git a/servers/execution/runner/test/data/scripts/create b/servers/execution/runner/test/data/script/create similarity index 100% rename from servers/execution/runner/test/data/scripts/create rename to servers/execution/runner/test/data/script/create diff --git a/servers/execution/runner/test/e2e/commander.spec.ts b/servers/execution/runner/test/e2e/commander.spec.ts new file mode 100644 index 000000000..628eb6ca4 --- /dev/null +++ b/servers/execution/runner/test/e2e/commander.spec.ts @@ -0,0 +1,57 @@ +import { jest } from '@jest/globals'; +import CLI, { createCommand } from 'src/config/commander'; +import Keyv from 'keyv'; + +describe('Commander functionality', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('Should invoke commander correctly', async () => { + const [program, CLIOptionsExp] = createCommand('runner'); + const argvCall = jest.spyOn(program, 'parse'); + + await CLI(program, CLIOptionsExp); + + expect(argvCall).toHaveBeenCalled(); + expect(program.opts()).toEqual({ + config: 'runner.yaml', + }); + }); + + it('Should run without any flags', async () => { + const [program, CLIOptionsExp] = createCommand('runner'); + const CLIOptions = await CLI(program, CLIOptionsExp); + expect(CLIOptions).toBeInstanceOf(Keyv); + expect(CLIOptionsExp).toEqual(CLIOptions); + }); + + it('Should process config correctly', async () => { + const [program, CLIOptionsExp] = createCommand('runner'); + jest.replaceProperty(process, 'argv', [ + 'node', + 'main.js', + '--config', + 'runner.test.yaml', + ]); + await CLI(program, CLIOptionsExp); + + expect(program.opts()).toEqual({ + config: 'runner.test.yaml', + }); + }); + + it('Should throw exception of the config file is not found', async () => { + const [program, CLIOptionsExp] = createCommand('runner'); + jest.replaceProperty(process, 'argv', [ + 'node', + 'main.js', + '--config', + 'runner.test.yaml.nonexistent', + ]); + const callCLI = async () => { + await CLI(program, CLIOptionsExp); + }; + await expect(callCLI()).rejects.toThrow(Error); + }); +}); diff --git a/servers/execution/runner/test/e2e/options.spec.ts b/servers/execution/runner/test/e2e/options.spec.ts new file mode 100644 index 000000000..95a647d4f --- /dev/null +++ b/servers/execution/runner/test/e2e/options.spec.ts @@ -0,0 +1,138 @@ +import { Test } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import AppModule from 'src/app.module'; +import Keyv from 'keyv'; +import Config from 'src/config/configuration.service'; +import { getRequest, postRequest, queriesJSON, RequestBody } from './util'; + +const OptionsArray = [ + { + option: null, + testName: 'default configuration', + }, + { + option: async (): Promise => { + const keyv = new Keyv(); + await keyv.set('configFile', 'runner.yaml'); + return keyv; + }, + testName: 'configuration loaded from configuration file', + }, +]; + +OptionsArray.forEach((element) => { + describe(`Runner end-to-end tests with ${element.testName}`, () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + if (element.option !== null) { + const config = app.get(Config); + config.loadConfig(await element.option()); + } + await app.init(); + }); + afterEach(async () => { + await app.close(); + }); + + describe('POST /', () => { + const keys: (keyof typeof queriesJSON)[] = [ + 'valid', + 'invalid', + 'incorrect', + ]; + keys.forEach((key) => { + const query = queriesJSON[key]; + it(`execute ${key} command`, () => + postRequest({ + app, + route: '/', + HttpStatus: query.HttpStatus, + reqBody: query.reqBody, + resBody: query.resBody.POST, + })); + }); + }); + + describe('GET /', () => { + const keys: (keyof typeof queriesJSON)[] = [ + 'valid', + 'invalid', + 'incorrect', + ]; + keys.forEach((key) => { + const query = queriesJSON[key]; + it(`execution status of ${key} command`, async () => { + await postRequest({ + app, + route: '/', + HttpStatus: query.HttpStatus, + reqBody: query.reqBody, + resBody: query.resBody.POST, + }); + return getRequest({ + app, + route: '/', + HttpStatus: 200, + reqBody: {}, + resBody: query.resBody.GET, + }); + }); + }); + + it('execution status without any prior command executions', () => + getRequest({ + app, + route: '/', + HttpStatus: 200, + reqBody: {}, + resBody: { + name: 'none', + status: 'invalid', + logs: { stdout: '', stderr: '' }, + }, + })); + }); + + describe('GET /history', () => { + it('without any prior command executions', () => + getRequest({ + app, + route: '/history', + HttpStatus: 200, + reqBody: {}, + resBody: new Array(), + })); + + it('after multiple command executions', async () => { + const keys: (keyof typeof queriesJSON)[] = [ + 'valid', + 'invalid', + 'incorrect', + ]; + keys.forEach(async (key) => { + const query = queriesJSON[key]; + await postRequest({ + app, + route: '/', + HttpStatus: query.HttpStatus, + reqBody: query.reqBody, + resBody: query.resBody.POST, + }); + }); + return getRequest({ + app, + route: '/history', + HttpStatus: 200, + reqBody: {}, + resBody: [{ name: 'create' }, { name: 'configure' }], + }); + }); + }); + }); +}); diff --git a/servers/execution/runner/test/e2e/util.ts b/servers/execution/runner/test/e2e/util.ts new file mode 100644 index 000000000..5f24ab5b5 --- /dev/null +++ b/servers/execution/runner/test/e2e/util.ts @@ -0,0 +1,97 @@ +import supertest from 'supertest'; +import { INestApplication } from '@nestjs/common'; + +export interface RequestBody { + name?: string; + command?: string; +} + +type ResponseBody = { + message?: string; + error?: string; + statusCode?: number; + status?: string; + name?: string; + logs?: { stdout: string; stderr: string }; +}; + +type Query = { + app: INestApplication; + route: string; + HttpStatus: number; + reqBody: RequestBody; + resBody: ResponseBody | Array; +}; + +export function postRequest(query: Query) { + return supertest(query.app.getHttpServer()) + .post(query.route) + .send(query.reqBody) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(query.HttpStatus) + .expect(query.resBody); +} + +export function getRequest(query: Query) { + return supertest(query.app.getHttpServer()) + .get(query.route) + .send(query.reqBody) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json') + .expect(query.HttpStatus) + .expect(query.resBody); +} + +export const queriesJSON = { + valid: { + reqBody: { + name: 'create', + }, + HttpStatus: 200, + resBody: { + POST: { + status: 'success', + }, + GET: { + name: 'create', + status: 'valid', + logs: { stdout: 'hello world', stderr: '' }, + }, + }, + }, + invalid: { + reqBody: { + name: 'configure', + }, + HttpStatus: 400, + resBody: { + POST: { + status: 'invalid command', + }, + GET: { + name: 'configure', + status: 'invalid', + logs: { stdout: '', stderr: '' }, + }, + }, + }, + incorrect: { + reqBody: { + command: 'create', + }, + HttpStatus: 400, + resBody: { + POST: { + message: 'Validation Failed', + error: 'Bad Request', + statusCode: 400, + }, + GET: { + name: 'none', + status: 'invalid', + logs: { stdout: '', stderr: '' }, + }, + }, + }, +}; diff --git a/servers/execution/runner/test/integration/app.controller.spec.ts b/servers/execution/runner/test/integration/app.controller.spec.ts new file mode 100644 index 000000000..5d6a84d17 --- /dev/null +++ b/servers/execution/runner/test/integration/app.controller.spec.ts @@ -0,0 +1,122 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { Test } from '@nestjs/testing'; +import AppModule from 'src/app.module'; +import AppController from 'src/app.controller'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Response } from 'express'; +import { ExecuteCommandDto } from 'src/dto/command.dto'; +import { Manager } from 'src/interfaces/command.interface'; +import ExecaManager from 'src/execa-manager.service'; + +type ResponseBody = { + status: string; +}; + +let mockBlackhole: jest.Mock; + +describe('Test AppController', () => { + let app: INestApplication; + let controller: AppController; + let manager: Manager; + + async function newCommandTest( + executeCommandDto: ExecuteCommandDto, + httpStatus: HttpStatus, + resBody: ResponseBody, + ) { + mockBlackhole = jest.fn(); + const res: Response = {} as Response; + + res.status = function status(): Response { + mockBlackhole(httpStatus); + return res; + }; + res.send = function send(body: ResponseBody): Response { + mockBlackhole(body); + return res; + }; + const resStatus = jest.spyOn(res, 'status'); + const resSend = jest.spyOn(res, 'send'); + const spyManager = jest.spyOn(manager, 'newCommand'); + + await controller.newCommand(executeCommandDto, res); + + expect(spyManager).toHaveBeenCalledWith(executeCommandDto.name); + expect(resStatus).toHaveBeenCalledWith(httpStatus); + expect(resSend).toHaveBeenCalledWith(resBody); + } + + beforeEach(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + controller = app.get(AppController); + manager = app.get(ExecaManager); + }); + + afterEach(() => jest.resetAllMocks()); + + it('Should start with empty command history', () => { + expect(controller.getHistory()).toEqual([]); + }); + + it('should execute an available command', async () => { + await newCommandTest({ name: 'create' }, HttpStatus.OK, { + status: 'success', + }); + }); + + it('should not execute an unavailable command', async () => { + await newCommandTest({ name: 'test' }, HttpStatus.BAD_REQUEST, { + status: 'invalid command', + }); + }); + + it('Should have correct command history', async () => { + await newCommandTest({ name: 'create' }, HttpStatus.OK, { + status: 'success', + }); + await newCommandTest({ name: 'test' }, HttpStatus.BAD_REQUEST, { + status: 'invalid command', + }); + expect(controller.getHistory()).toEqual([ + { + name: 'create', + }, + { + name: 'test', + }, + ]); + }); + + it('Should return correct command status', async () => { + await newCommandTest({ name: 'create' }, HttpStatus.OK, { + status: 'success', + }); + const successStatus = await controller.cmdStatus(); + + await newCommandTest({ name: 'test' }, HttpStatus.BAD_REQUEST, { + status: 'invalid command', + }); + const unsuccessStatus = await controller.cmdStatus(); + + expect(successStatus).toEqual({ + name: 'create', + status: 'valid', + logs: { + stdout: 'hello world', + stderr: '', + }, + }); + expect(unsuccessStatus).toEqual({ + name: 'test', + status: 'invalid', + logs: { + stdout: '', + stderr: '', + }, + }); + }); +}); diff --git a/servers/execution/runner/test/integration/execaManager.service.spec.ts b/servers/execution/runner/test/integration/execa-manager.service.spec.ts similarity index 77% rename from servers/execution/runner/test/integration/execaManager.service.spec.ts rename to servers/execution/runner/test/integration/execa-manager.service.spec.ts index 9c3c89f5f..59d00ddcb 100644 --- a/servers/execution/runner/test/integration/execaManager.service.spec.ts +++ b/servers/execution/runner/test/integration/execa-manager.service.spec.ts @@ -1,12 +1,24 @@ -import { describe, expect, it } from '@jest/globals'; -import ExecaManager from 'src/execaManager.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { describe, expect, it, beforeEach } from '@jest/globals'; +import ExecaManager from 'src/execa-manager.service'; import { Manager, CommandStatus } from 'src/interfaces/command.interface'; import { ExecuteCommandDto } from 'src/dto/command.dto'; +import Queue from 'src/queue.service'; +import Config from 'src/config/configuration.service'; describe('Check execution manager based on execa library', () => { + let dt: Manager; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ExecaManager, Queue, Config], + }).compile(); + + dt = module.get(ExecaManager); + }); + it('Should create object', async () => { try { - const dt: Manager = new ExecaManager(); expect(dt).toBeInstanceOf(ExecaManager); } catch (error) { expect(fail); @@ -14,7 +26,6 @@ describe('Check execution manager based on execa library', () => { }); it('Should execute a valid command', async () => { - const dt: Manager = new ExecaManager(); let status: boolean = false; let logs: Map = new Map(); @@ -26,7 +37,6 @@ describe('Check execution manager based on execa library', () => { }); it('Should not execute an invalid command', async () => { - const dt: Manager = new ExecaManager(); let status: boolean = true; [status] = await dt.newCommand('asdfghjkl'); @@ -43,14 +53,12 @@ describe('Check execution manager based on execa library', () => { stderr: '', }, }; - const dt: Manager = new ExecaManager(); const commandStatus: CommandStatus = dt.checkStatus(); expect(commandStatus).toEqual(expPhaseStatus); }); it('Should hold correct history of command executions', async () => { - const dt: Manager = new ExecaManager(); const status: boolean[] = []; const pastPhases: Array = [ { diff --git a/servers/execution/runner/test/integration/execaRunner.spec.ts b/servers/execution/runner/test/integration/execa-runner.spec.ts similarity index 96% rename from servers/execution/runner/test/integration/execaRunner.spec.ts rename to servers/execution/runner/test/integration/execa-runner.spec.ts index 87ff86ac8..624ce490a 100644 --- a/servers/execution/runner/test/integration/execaRunner.spec.ts +++ b/servers/execution/runner/test/integration/execa-runner.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from '@jest/globals'; import Runner from 'src/interfaces/runner.interface'; -import ExecaRunner from 'src/execaRunner'; +import ExecaRunner from 'src/execa-runner'; describe('check command Runner based on execa library', () => { it('should execute a valid command', async () => { diff --git a/servers/execution/runner/test/integration/options.service.spec.ts b/servers/execution/runner/test/integration/options.service.spec.ts new file mode 100644 index 000000000..ff76fed8d --- /dev/null +++ b/servers/execution/runner/test/integration/options.service.spec.ts @@ -0,0 +1,36 @@ +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import Config from 'src/config/configuration.service'; +import Keyv from 'keyv'; +import resolveFile from 'src/config/util'; + +describe('Check Configuration Service', () => { + let config: Config; + + beforeEach(() => { + config = new Config(); + }); + + it('Should create a valid config object', async () => { + expect(config).toBeInstanceOf(Config); + }); + + it('Should have correct default config after creation', async () => { + expect(config.getPort()).toEqual(5000); + expect(config.permitCommands()).toHaveLength(0); + expect(config.getLocation()).toEqual('script'); + }); + + it('Should load correct configuration', async () => { + const CLIOptions = new Keyv(); + await CLIOptions.set('configFile', 'runner.test.yaml'); + const spyOnCLIOptions = jest.spyOn(CLIOptions, 'get'); + + await config.loadConfig(CLIOptions); + + expect(config.getPort()).toEqual(5002); + expect(config.permitCommands()).toHaveLength(1); + expect(config.permitCommands()).toContain('create'); + expect(config.getLocation()).toEqual(resolveFile('script')); + expect(spyOnCLIOptions).toBeCalled(); + }); +}); diff --git a/servers/execution/runner/test/integration/queue.service.spec.ts b/servers/execution/runner/test/integration/queue.service.spec.ts index 40762247a..58ae09cc1 100644 --- a/servers/execution/runner/test/integration/queue.service.spec.ts +++ b/servers/execution/runner/test/integration/queue.service.spec.ts @@ -1,50 +1,53 @@ -import { describe, it, expect } from '@jest/globals'; +import { Test, TestingModule } from '@nestjs/testing'; +import { describe, it, expect, beforeEach } from '@jest/globals'; import Queue from 'src/queue.service'; import { Command } from 'src/interfaces/command.interface'; -import ExecaRunner from 'src/execaRunner'; +import RunnerFactory from 'src/runner-factory.service'; const commands: Command[] = [ { name: 'hello', status: 'valid', - task: new ExecaRunner(''), + task: RunnerFactory.create('cd .'), }, { name: 'world', status: 'valid', - task: new ExecaRunner(''), + task: RunnerFactory.create('cd .'), }, { name: 'terminate', status: 'invalid', - task: new ExecaRunner(''), + task: RunnerFactory.create('cd .'), }, ]; describe('check Queue service', () => { - it('should store a command', async () => { - const queue: Queue = new Queue(); + let queue: Queue; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [Queue], + }).compile(); + queue = module.get(Queue); + }); + + it('should store a command', async () => { expect(queue.enqueue(commands[0])).toBe(true); }); it('should return active command as undefined when queue is empty', async () => { - const queue: Queue = new Queue(); - expect(queue.activeCommand()).toBe(undefined); }); it('should return active command when queue is non-empty', async () => { - const queue: Queue = new Queue(); - queue.enqueue(commands[0]); expect(queue.activeCommand()).toBe(commands[0]); }); it('should return correct active command when queue has more than one command', async () => { - const queue: Queue = new Queue(); - queue.enqueue(commands[0]); queue.enqueue(commands[1]); @@ -52,13 +55,10 @@ describe('check Queue service', () => { }); it('should return empty array when queue is empty', async () => { - const queue: Queue = new Queue(); - expect(queue.checkHistory()).toStrictEqual([]); }); it('should return correct command history when queue has more than one command', async () => { - const queue: Queue = new Queue(); const history = [ { name: 'hello', diff --git a/servers/execution/runner/test/integration/runner-factory.service.spec.ts b/servers/execution/runner/test/integration/runner-factory.service.spec.ts new file mode 100644 index 000000000..0f9864dc1 --- /dev/null +++ b/servers/execution/runner/test/integration/runner-factory.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { describe, it, expect, beforeEach } from '@jest/globals'; +import RunnerFactory from 'src/runner-factory.service'; +import Runner from 'src/interfaces/runner.interface'; +import ExecaRunner from 'src/execa-runner'; + +describe('Check RunnerFactoryService', () => { + let service: RunnerFactory; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RunnerFactory], + }).compile(); + + service = module.get(RunnerFactory); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create new ExecaRunner object', () => { + const runner: Runner = RunnerFactory.create('cd .'); + expect(runner).toBeInstanceOf(ExecaRunner); + }); +}); diff --git a/servers/execution/runner/test/unit/util.spec.ts b/servers/execution/runner/test/unit/util.spec.ts new file mode 100644 index 000000000..a96fa7ffd --- /dev/null +++ b/servers/execution/runner/test/unit/util.spec.ts @@ -0,0 +1,16 @@ +import path from 'node:path'; +import { describe, expect, it } from '@jest/globals'; +import resolveFile from 'src/config/util'; + +describe('Check utils library', () => { + it('Should correctly resolve absolute path', () => { + if (process.platform === 'win32') { + // prettier-ignore + // eslint-disable-next-line no-useless-escape + expect(resolveFile('C:\Users\dtaas')).toEqual('C:\Users\dtaas'); + } else { + expect(resolveFile('/opt/dtaas')).toEqual('/opt/dtaas'); + } + expect(resolveFile('runner')).toEqual(path.join(process.cwd(), 'runner')); + }); +}); diff --git a/servers/execution/runner/yarn.lock b/servers/execution/runner/yarn.lock index 0c54b8430..3e2fb0b6c 100644 --- a/servers/execution/runner/yarn.lock +++ b/servers/execution/runner/yarn.lock @@ -299,6 +299,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.24.0" +"@babel/runtime@^7.21.0": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -2106,6 +2113,11 @@ commander@4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.0.0.tgz#b929db6df8546080adfd004ab215ed48cf6f2592" + integrity sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2147,6 +2159,21 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +concurrently@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-8.2.2.tgz#353141985c198cfa5e4a3ef90082c336b5851784" + integrity sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg== + dependencies: + chalk "^4.1.2" + date-fns "^2.30.0" + lodash "^4.17.21" + rxjs "^7.8.1" + shell-quote "^1.8.1" + spawn-command "0.0.2" + supports-color "^8.1.1" + tree-kill "^1.2.2" + yargs "^17.7.2" + confusing-browser-globals@^1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" @@ -2277,6 +2304,13 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4268,7 +4302,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -keyv@^4.0.0, keyv@^4.5.3: +keyv@^4.0.0, keyv@^4.5.3, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -5131,6 +5165,11 @@ reflect-metadata@^0.2.2: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -5404,6 +5443,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + shelljs@0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" @@ -5483,6 +5527,11 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +spawn-command@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" + integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -5684,7 +5733,7 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -5796,7 +5845,7 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== -tree-kill@1.2.2: +tree-kill@1.2.2, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== @@ -6256,7 +6305,7 @@ yargs-parser@21.1.1, yargs-parser@^21.0.1, yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.3.1: +yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==