diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..f7210bb Binary files /dev/null and b/.DS_Store differ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a34174e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + // Use IntelliSense to learn about possible 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": "server-dev", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run-script", "dev" + ], + "cwd": "${workspaceRoot}/server/", + }, + { + "type": "node", + "request": "launch", + "name": "server-test-watch", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run-script", "test:watch" + ], + "cwd": "${workspaceRoot}/server/", + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e2c0be0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "files.exclude": { + "**/.git": false, + "**/.svn": false, + "**/.hg": false, + "**/CVS": false, + "**/__pycache__": false, + "test/*context.*": false, + "test/*mocks": false, + "test/*mocks.*": false, + "test/*test": false, + "test/*test.*": false, + "test/*test.js": false, + "test/*test.js.*": false, + "test/*todo": false, + "test/*todo.*": false, + "**/*.js": true, + "**/*.map": true + } +} \ No newline at end of file diff --git a/server/.eslintrc.cjs b/server/.eslintrc.cjs new file mode 100644 index 0000000..9f3ef29 --- /dev/null +++ b/server/.eslintrc.cjs @@ -0,0 +1,31 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + env: { + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + ], + settings: { + react: { + version: "detect", + }, + }, + rules: { + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", // or "error" + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + }, + ignorePatterns: ["**/generated/*"], +}; diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..4d4a8bd --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,8 @@ +*.js +*.js.map +.nyc_output +.DS_Store +node_modules/ +coverage +package-lock.json +.*.swp diff --git a/server/.npmignore b/server/.npmignore new file mode 100644 index 0000000..bd6c7fe --- /dev/null +++ b/server/.npmignore @@ -0,0 +1,2 @@ +*.ts +!*.d.ts \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..cb0043a --- /dev/null +++ b/server/README.md @@ -0,0 +1 @@ +Веб-сервис для отладки Robossembler Framework \ No newline at end of file diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..3a35aae --- /dev/null +++ b/server/package.json @@ -0,0 +1,42 @@ +{ + "name": "step-by-step-server", + "version": "0.0.1", + "type": "module", + "description": "", + "main": "index.js", + "scripts": { + "pretest": "tsc", + "test": "node ./test/test.js", + "test:watch": "tsc-watch --onSuccess 'node ./test/test.js'" + }, + "author": "IDONTSUDO", + "devDependencies": { + "@testdeck/mocha": "latest", + "@types/chai": "latest", + "@types/md5": "^2.3.2", + "@types/mocha": "latest", + "@types/node": "^20.4.8", + "@typescript-eslint/eslint-plugin": "^6.4.0", + "@typescript-eslint/parser": "^6.4.0", + "chai": "latest", + "eslint": "^8.47.0", + "mocha": "latest", + "nyc": "latest", + "source-map-support": "latest", + "tslint": "latest", + "typescript": "^5.1.6" + }, + "dependencies": { + "@grpc/grpc-js": "^1.9.0", + "babel-register": "^6.26.0", + "concurrently": "^8.2.0", + "first-di": "^1.0.11", + "md5": "^2.3.0", + "node-watch": "^0.7.4", + "nodemon": "^3.0.1", + "reflect-metadata": "^0.1.13", + "spark-md5": "^3.0.2", + "ts-md5": "^1.3.1", + "tsc-watch": "^6.0.4" + } +} diff --git a/server/src/core/di/env.ts b/server/src/core/di/env.ts new file mode 100644 index 0000000..8082916 --- /dev/null +++ b/server/src/core/di/env.ts @@ -0,0 +1,45 @@ +import { reflection } from "first-di"; + +@reflection +export class IEnv{ + rootFolder!: string; + constructor(){ + + } + toStringEnv(){ + return '' + } + static env(){ + return '' + } +} + +@reflection +export class DevEnv implements IEnv { + rootFolder:string; + constructor(rootFolder:string){ + this.rootFolder = rootFolder + } + toStringEnv(): string { + return DevEnv.env() + } + static env(){ + return 'DevEnv' + + } +} + +@reflection +export class UnitTestEnv implements IEnv{ + rootFolder:string; + constructor(rootFolder:string){ + this.rootFolder = rootFolder + } + toStringEnv(): string { + return UnitTestEnv.env() + } + static env(){ + return 'UnitTestEnv' + + } +} \ No newline at end of file diff --git a/server/src/core/di/register_di.ts b/server/src/core/di/register_di.ts new file mode 100644 index 0000000..4ccb1e8 --- /dev/null +++ b/server/src/core/di/register_di.ts @@ -0,0 +1,48 @@ +import { override } from "first-di"; +import { DevEnv, IEnv , UnitTestEnv } from "./env.js"; +import { MetaDataFileManagerModel } from "../model/meta_data_file_manager_model.js"; + +export default function locator(env: IEnv){ + envRegister(env); + registerRepository(env); + registerController(env); + registerService(env); + override(MetaDataFileManagerModel, MetaDataFileManagerModel) +} +const envRegister = (env: IEnv) => { + switch (env.toStringEnv()) { + case UnitTestEnv.env(): + override(IEnv, UnitTestEnv); + return; + case "DevEnv": + override(IEnv, DevEnv); + return; + } +}; +const registerRepository = (env: IEnv) => { + switch (env.toStringEnv()) { + case UnitTestEnv.env(): + override(IEnv, UnitTestEnv); + + return; + case DevEnv.env(): + override(IEnv, DevEnv); + return; + } +}; +const registerController = (env: IEnv) => { + switch (env.toStringEnv()) { + case UnitTestEnv.env(): + return; + case DevEnv.env(): + return; + } +}; +const registerService = (env: IEnv) => { + switch (env.toStringEnv()) { + case UnitTestEnv.env(): + return; + case DevEnv.env(): + return; + } +}; diff --git a/server/src/core/helper/cancelable_promise.ts b/server/src/core/helper/cancelable_promise.ts new file mode 100644 index 0000000..86d59be --- /dev/null +++ b/server/src/core/helper/cancelable_promise.ts @@ -0,0 +1,248 @@ +const toStringTag: typeof Symbol.toStringTag = + typeof Symbol !== 'undefined' ? Symbol.toStringTag : ('@@toStringTag' as any); + +class CancelablePromiseInternal { + #internals: Internals; + #promise: Promise; + + [toStringTag] = 'CancelablePromise'; + + constructor({ + executor = () => {}, + internals = defaultInternals(), + promise = new Promise((resolve, reject) => + executor(resolve, reject, (onCancel) => { + internals.onCancelList.push(onCancel); + }) + ), + }: { + executor?: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: (cancelHandler: () => void) => void + ) => void; + internals?: Internals; + promise?: Promise; + }) { + this.cancel = this.cancel.bind(this); + this.#internals = internals; + this.#promise = + promise || + new Promise((resolve, reject) => + executor(resolve, reject, (onCancel) => { + internals.onCancelList.push(onCancel); + }) + ); + } + + then( + onfulfilled?: + | (( + value: T + ) => TResult1 | PromiseLike | CancelablePromise) + | undefined + | null, + onrejected?: + | (( + reason: any + ) => TResult2 | PromiseLike | CancelablePromise) + | undefined + | null + ): CancelablePromise { + return makeCancelable( + this.#promise.then( + createCallback(onfulfilled, this.#internals), + createCallback(onrejected, this.#internals) + ), + this.#internals + ); + } + + catch( + onrejected?: + | (( + reason: any + ) => TResult | PromiseLike | CancelablePromise) + | undefined + | null + ): CancelablePromise { + return makeCancelable( + this.#promise.catch(createCallback(onrejected, this.#internals)), + this.#internals + ); + } + + finally( + onfinally?: (() => void) | undefined | null, + runWhenCanceled?: boolean + ): CancelablePromise { + if (runWhenCanceled) { + this.#internals.onCancelList.push(onfinally); + } + return makeCancelable( + this.#promise.finally( + createCallback(() => { + if (onfinally) { + if (runWhenCanceled) { + this.#internals.onCancelList = + this.#internals.onCancelList.filter( + (callback) => callback !== onfinally + ); + } + return onfinally(); + } + }, this.#internals) + ), + this.#internals + ); + } + + cancel(): void { + this.#internals.isCanceled = true; + const callbacks = this.#internals.onCancelList; + this.#internals.onCancelList = []; + for (const callback of callbacks) { + if (typeof callback === 'function') { + try { + callback(); + } catch (err) { + console.error(err); + } + } + } + } + + isCanceled(): boolean { + return this.#internals.isCanceled === true; + } +} + +export class CancelablePromise extends CancelablePromiseInternal { + static all = function all(iterable: any) { + return makeAllCancelable(iterable, Promise.all(iterable)); + } as CancelablePromiseOverloads['all']; + + static allSettled = function allSettled(iterable: any) { + return makeAllCancelable(iterable, Promise.allSettled(iterable)); + } as CancelablePromiseOverloads['allSettled']; + + static any = function any(iterable: any) { + return makeAllCancelable(iterable, Promise.any(iterable)); + } as CancelablePromiseOverloads['any']; + + static race = function race(iterable) { + return makeAllCancelable(iterable, Promise.race(iterable)); + } as CancelablePromiseOverloads['race']; + + static resolve = function resolve(value) { + return cancelable(Promise.resolve(value)); + } as CancelablePromiseOverloads['resolve']; + + static reject = function reject(reason) { + return cancelable(Promise.reject(reason)); + } as CancelablePromiseOverloads['reject']; + + static isCancelable = isCancelablePromise; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + onCancel: (cancelHandler: () => void) => void + ) => void + ) { + super({ executor }); + } +} + +export default CancelablePromise; + +export function cancelable(promise: Promise): CancelablePromise { + return makeCancelable(promise, defaultInternals()); +} + +export function isCancelablePromise(promise: any): boolean { + return ( + promise instanceof CancelablePromise || + promise instanceof CancelablePromiseInternal + ); +} + +function createCallback(onResult: any, internals: Internals):any { + if (onResult) { + return (arg?: any) => { + if (!internals.isCanceled) { + const result = onResult(arg); + if (isCancelablePromise(result)) { + internals.onCancelList.push(result.cancel); + } + return result; + } + return arg; + }; + } +} + +function makeCancelable(promise: Promise, internals: Internals) { + return new CancelablePromiseInternal({ + internals, + promise, + }) as CancelablePromise; +} + +function makeAllCancelable(iterable: any, promise: Promise) { + const internals = defaultInternals(); + internals.onCancelList.push(() => { + for (const resolvable of iterable) { + if (isCancelablePromise(resolvable)) { + resolvable.cancel(); + } + } + }); + return new CancelablePromiseInternal({ internals, promise }); +} + +function defaultInternals(): Internals { + return { isCanceled: false, onCancelList: [] }; +} + +interface Internals { + isCanceled: boolean; + onCancelList: any[]; +} + +interface CancelablePromiseOverloads { + all( + values: T + ): CancelablePromise<{ -readonly [P in keyof T]: Awaited }>; + + allSettled( + values: T + ): CancelablePromise<{ + -readonly [P in keyof T]: PromiseSettledResult>; + }>; + + allSettled( + values: Iterable | CancelablePromise> + ): CancelablePromise>[]>; + + any( + values: T + ): CancelablePromise>; + + any( + values: Iterable | CancelablePromise> + ): CancelablePromise>; + + race( + values: T + ): CancelablePromise>; + + resolve(): CancelablePromise; + + resolve( + value: T | PromiseLike | CancelablePromise + ): CancelablePromise; + + reject(reason?: any): CancelablePromise; +} \ No newline at end of file diff --git a/server/src/core/helper/delay.ts b/server/src/core/helper/delay.ts new file mode 100644 index 0000000..e502651 --- /dev/null +++ b/server/src/core/helper/delay.ts @@ -0,0 +1,3 @@ +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/server/src/core/helper/result.ts b/server/src/core/helper/result.ts new file mode 100644 index 0000000..1f24112 --- /dev/null +++ b/server/src/core/helper/result.ts @@ -0,0 +1,533 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-unnecessary-type-constraint */ +/* eslint-disable @typescript-eslint/no-namespace */ + + +function isAsyncFn(fn: Function) { + return fn.constructor.name === "AsyncFunction"; +} + +function isResult(value: unknown): value is Result { + return value instanceof Ok || value instanceof Err; +} + +interface SyncThenable { + isSync: true; + then Promise>(cb: Fn): ReturnType; + then any>(cb: Fn): SyncThenable; +} + + +function syncThenable(): SyncThenable { + function then Promise>(cb: Fn): ReturnType; + function then any>(cb: Fn): SyncThenable; + function then(cb: any) { + const result = cb(); + if (result instanceof Promise) { + return result; + } + + return syncThenable(); + } + + return { + isSync: true, + then, + }; +} + +function forEachValueThunkOrPromise( + items: unknown[], + execFn: (value: T) => boolean, + foldFn: () => unknown +) { + let shouldBreak = false; + + const result: any = items.reduce((prev: { then: Function }, valueOrThunk) => { + return prev.then(() => { + if (shouldBreak) { + return null; + } + + function run(value: T) { + const isSuccess = execFn(value); + if (!isSuccess) { + shouldBreak = true; + } + } + + const valueOrPromise = + typeof valueOrThunk === "function" ? valueOrThunk() : valueOrThunk; + + if (valueOrPromise instanceof Promise) { + return valueOrPromise.then(run); + } + + return run(valueOrPromise); + }); + }, syncThenable()); + + if ((result as SyncThenable).isSync) { + return foldFn(); + } + + return result.then(() => { + return foldFn(); + }); +} + +export type Result< + ErrorType, + OkType, + RollbackFn extends RollbackFunction = any +> = Ok | Err; + +interface IResult { + isSuccess(): this is Ok; + + isFailure(): this is Err; + + getOrNull(): OkType | null; + + toString(): string; + + inspect(): string; + + fold( + onSuccess: (value: OkType) => R, + onFailure: (error: ErrorType) => R + ): R; + fold( + onSuccess: (value: OkType) => Promise, + onFailure: (error: ErrorType) => Promise + ): Promise; + + getOrDefault(defaultValue: OkType): OkType; + + getOrElse(onFailure: (error: ErrorType) => OkType): OkType; + getOrElse(onFailure: (error: ErrorType) => Promise): Promise; + + getOrThrow(): OkType; + + map( + fn: (value: OkType) => Promise + ): Promise< + JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + > + >; + map( + fn: (value: OkType) => T + ): JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + >; + + rollback(): Result | Promise>; +} + +type InferErrorType> = T extends Result< + infer Errortype, + any, + any +> + ? Errortype + : never; + +type InferOkType> = T extends Result< + any, + infer OkType, + any +> + ? OkType + : never; + +type JoinErrorTypes> = Result< + ErrorType | InferErrorType, + InferOkType, + any +>; + +type ExtractErrorTypes = { + [Index in keyof Tuple]: Tuple[Index] extends Result + ? InferErrorType + : never; +}[number]; + +type MapResultTupleToOkTypeTuple = { + [Index in keyof Tuple]: Tuple[Index] extends Result + ? InferOkType + : never; +}; + +type RollbackFunction = (() => void) | (() => Promise); + +type HasAsyncRollbackFunction = { + [Index in keyof T]: T[Index] extends () => Promise | infer U + ? U extends Result Promise> + ? true + : false + : false; +}[number] extends false + ? false + : true; + +type UnwrapThunks = { + [Index in keyof T]: T[Index] extends () => Promise + ? U + : T[Index] extends () => infer U + ? U + : T[Index]; +}; + +type HasAsyncThunk = { + [Index in keyof T]: T[Index] extends () => Promise ? true : false; +}[number] extends false + ? false + : true; + +type PromiseReturnType any> = T extends ( + ...args: any +) => Promise + ? U + : never; + +export namespace Result { + export function ok< + ErrorType extends unknown, + OkType, + RollbackFn extends RollbackFunction = any + >( + value?: OkType, + rollbackFn?: RollbackFn + ): Result { + return new Ok( + value || null!, + rollbackFn + ) as any; + } + + export function error< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction = any + >( + error: ErrorType, + rollbackFn?: RollbackFn + ): Result { + return new Err(error, rollbackFn); + } + + type SafeReturnType = T extends Result + ? Result, InferOkType, never> + : Result; + + export function safe( + fn: () => Promise + ): Promise>; + export function safe(fn: () => T): SafeReturnType; + export function safe( + err: ErrorType | (new (...args: any[]) => ErrorType), + fn: () => Promise + ): Promise>; + export function safe( + err: ErrorType | (new (...args: any[]) => ErrorType), + fn: () => T + ): SafeReturnType; + export function safe(errOrFn: any, fn?: any) { + const hasCustomError = fn !== undefined; + + const execute = hasCustomError ? fn : errOrFn; + + function getError(caughtError: Error) { + if (!hasCustomError) { + return caughtError; + } + + if (typeof errOrFn === "function") { + return new errOrFn(caughtError); + } + + return errOrFn; + } + + try { + const resultOrPromise = execute(); + + if (resultOrPromise instanceof Promise) { + return resultOrPromise + .then((okValue) => { + return isResult(okValue) ? okValue : Result.ok(okValue); + }) + .catch((caughtError) => error(getError(caughtError))); + } + + return isResult(resultOrPromise) + ? resultOrPromise + : Result.ok(resultOrPromise); + } catch (caughtError) { + return error(getError(caughtError as Error)); + } + } + + type CombineResult< + T extends (unknown | (() => unknown) | (() => Promise))[] + > = Result< + ExtractErrorTypes>, + MapResultTupleToOkTypeTuple>, + HasAsyncRollbackFunction extends true ? () => Promise : () => void + >; + + export function combine< + T extends (unknown | (() => unknown) | (() => Promise))[] + >( + ...items: T + ): HasAsyncThunk extends true + ? Promise> + : CombineResult { + if (!items.length) { + throw new Error("Expected at least 1 argument"); + } + + const values: unknown[] = []; + const rollbacks: RollbackFunction[] = []; + let error: Err | null = null; + + function rollback() { + const reversedRollbacks = rollbacks.reverse(); + const wrappedRollbackFns = reversedRollbacks.map((fn) => Result.wrap(fn)); + + let error: Err | null = null; + + return forEachValueThunkOrPromise( + wrappedRollbackFns, + (result: Result) => { + if (result.isFailure()) { + error = Result.error(result.error) as any; + return false; + } + + return true; + }, + () => error || ok() + ); + } + + return forEachValueThunkOrPromise( + items, + (result: Result) => { + if (result.isFailure()) { + error = Result.error(result.error, rollback) as any; + return false; + } + + values.push(result.value); + rollbacks.push(() => result.rollback()); + return true; + }, + () => error || ok(values, rollback) + ); + } + + export function wrap Promise>( + fn: Fn + ): ( + ...args: Parameters + ) => Promise, never>>; + export function wrap any>( + fn: Fn + ): (...args: Parameters) => Result, never>; + export function wrap(fn: any) { + return function wrapped(...args: any) { + try { + const resultOrPromise = fn(...args); + + if (resultOrPromise instanceof Promise) { + return resultOrPromise + .then((okValue) => Result.ok(okValue)) + .catch((err) => error(err)); + } + + return ok(resultOrPromise); + } catch (err) { + return error(err); + } + }; + } +} + +abstract class Base< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction +> implements IResult +{ + constructor(protected readonly rollbackFn?: RollbackFn) {} + + errorOrNull(): ErrorType | null { + if (this.isSuccess()) { + return null; + } + + return (this as any).error as ErrorType; + } + + getOrNull(): OkType | null { + if (this.isFailure()) { + return null; + } + + return (this as any).value as OkType; + } + + toString(): string { + throw new Error("Method not implemented."); + } + inspect(): string { + return this.toString(); + } + + fold( + onSuccess: (value: OkType) => R, + onFailure: (error: ErrorType) => R + ): R; + fold( + onSuccess: (value: OkType) => Promise, + onFailure: (error: ErrorType) => Promise + ): Promise; + fold(onSuccess: any, onFailure: any) { + if (this.isFailure()) { + return onFailure(this.error); + } + + return onSuccess((this as any).value as OkType); + } + + getOrDefault(defaultValue: OkType): OkType { + if (this.isSuccess()) { + return this.value; + } + + return defaultValue; + } + + getOrElse(onFailure: (error: ErrorType) => OkType): OkType; + getOrElse(onFailure: (error: ErrorType) => Promise): Promise; + getOrElse(onFailure: any) { + if (this.isSuccess()) { + return isAsyncFn(onFailure) ? Promise.resolve(this.value) : this.value; + } + + return onFailure((this as any).error as ErrorType); + } + + getOrThrow(): OkType { + if (this.isFailure()) { + throw this.error; + } + + return (this as any).value as OkType; + } + + isSuccess(): this is Ok { + throw new Error("Method not implemented."); + } + isFailure(): this is Err { + throw new Error("Method not implemented."); + } + + map( + fn: (value: OkType) => Promise + ): Promise< + JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + > + >; + map( + fn: (value: OkType) => T + ): JoinErrorTypes< + ErrorType, + T extends Result ? T : Result + >; + map(fn: any) { + if (this.isFailure()) { + return isAsyncFn(fn) ? Promise.resolve(this) : this; + } + + const result = Result.safe(() => fn((this as any).value) as any); + + return result as any; + } + + rollback(): RollbackFn extends RollbackFunction + ? RollbackFn extends () => Promise + ? Promise> + : Result + : void { + if (this.rollbackFn) { + return this.rollbackFn() as any; + } + + return null as any; + } +} + +class Ok< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction +> extends Base { + public readonly value: OkType; + + constructor(val: OkType, rollbackFn?: RollbackFn) { + super(rollbackFn); + this.value = val; + } + + isSuccess(): this is Ok { + return true; + } + + isFailure(): this is Err { + return false; + } + + toString(): string { + return `Result.Ok(${this.value})`; + } + + forward(): Result { + return Result.ok(this.value); + } +} + +class Err< + ErrorType extends unknown, + OkType extends unknown, + RollbackFn extends RollbackFunction +> extends Base { + public readonly error: ErrorType; + + constructor(err: ErrorType, rollbackFn?: RollbackFn) { + super(rollbackFn); + this.error = err; + } + + isSuccess(): this is Ok { + return false; + } + + isFailure(): this is Err { + return true; + } + + toString(): string { + return `Result.Error(${this.error})`; + } + + forward(): Result { + return Result.error(this.error); + } +} diff --git a/server/src/core/helper/typed_event.ts b/server/src/core/helper/typed_event.ts new file mode 100644 index 0000000..3dbc96e --- /dev/null +++ b/server/src/core/helper/typed_event.ts @@ -0,0 +1,50 @@ +export interface Listener { + (event: T): any; +} + +export interface Disposable { + dispose():void; +} + + +export class TypedEvent { + private listeners: Listener[] = []; + public listenersOncer: Listener[] = []; + + on = (listener: Listener): Disposable => { + this.listeners.push(listener); + return { + dispose: () => this.off(listener), + }; + }; + + once = (listener: Listener): void => { + this.listenersOncer.push(listener); + }; + + off = (listener: Listener) => { + const callbackIndex = this.listeners.indexOf( + listener + ); + if (callbackIndex > -1) + this.listeners.splice(callbackIndex, 1); + }; + + emit = (event: T) => { + /** Обновить слушателей */ + this.listeners.forEach((listener) => + listener(event) + ); + + /** Очистить очередь единожды */ + if (this.listenersOncer.length > 0) { + const toCall = this.listenersOncer; + this.listenersOncer = []; + toCall.forEach((listener) => listener(event)); + } + }; + + pipe = (te: TypedEvent): Disposable => { + return this.on((e) => te.emit(e)); + }; +} \ No newline at end of file diff --git a/server/src/core/helper/worker_computed.ts b/server/src/core/helper/worker_computed.ts new file mode 100644 index 0000000..3d19686 --- /dev/null +++ b/server/src/core/helper/worker_computed.ts @@ -0,0 +1,78 @@ +import { EXEC_EVENT, EXEC_TYPE, ExecError } from "../model/exec_error_model.js"; +import * as cp from "child_process"; +import { ExecutorResult } from "../model/executor_result.js"; + +export enum WorkerType { + EXEC = "EXEC", + SPAWN = "SPAWN", +} + +export interface WorkerDataExec { + command: string; + execPath: string; + type: WorkerType; + cliArgs:Array | undefined +} + +process.on("message", async (message) => { + const workerData = message as WorkerDataExec; + if (workerData.type == WorkerType.SPAWN) { + const subprocess = cp.spawn(workerData.command, workerData.cliArgs, { + cwd: workerData.execPath, + }); + + subprocess.stdout.on("data", (data: Buffer) => { + if (process.send) { + process.send({ + type: EXEC_TYPE.SPAWN.toString(), + event: EXEC_EVENT.PROGRESS.toString(), + data: data.toString(), + }); + } + }); + subprocess.on('close', (_code) =>{ + if (process.send) { + process.send({ + type: EXEC_TYPE.SPAWN.toString(), + event: EXEC_EVENT.END.toString(), + data:null + }); + } + }) + process.on("uncaughtException", (error) => { + if (process.send) { + process.send({ + command: workerData.command, + execPath: workerData.execPath, + error: error, + }); + } + }); + } else if (workerData.type == WorkerType.EXEC) { + try { + const result = await exec(workerData.command, { + cwd: workerData.execPath, + }); + if (process.send) { + process.send( + new ExecutorResult(EXEC_TYPE.EXEC, EXEC_EVENT.END, result) + ); + } + } catch (error) { + if (process.send) { + process.send(new ExecError(workerData.command, error )); + } + } + } +}); + +async function exec( + cmd: string, + opts: cp.ExecOptions & { trim?: boolean } = {} +): Promise { + return new Promise((c, e) => { + cp.exec(cmd, { env: process.env, ...opts }, (err, stdout) => { + return err ? e(err) : c(opts.trim ? stdout.trim() : stdout); + }); + }); +} diff --git a/server/src/core/model/exec_error_model.ts b/server/src/core/model/exec_error_model.ts new file mode 100644 index 0000000..4fd2dc5 --- /dev/null +++ b/server/src/core/model/exec_error_model.ts @@ -0,0 +1,61 @@ +export class ExecError extends Error { + static isExecError(e: any) { + if ("type" in e && "script" in e && "unixTime" in e) { + return new ExecError(e.type, e.event, e.data); + } + return; + } + script: string; + unixTime: number; + type = EXEC_TYPE.EXEC; + + constructor( + script: string, + ...args: any + ) { + super(...args); + this.script = script; + this.unixTime = Date.now(); + } +} + +export class SpawnError extends Error { + environment: string; + script: string; + unixTime: number; + type = EXEC_TYPE.SPAWN; + + constructor( + environment: string, + script: string, + ...args: any + ) { + super(...args); + this.environment = environment; + this.script = script; + this.unixTime = Date.now(); + } + static isError(errorType: any): void | SpawnError { + if ( + "command" in errorType && + "error" in errorType && + "execPath" in errorType + ) { + return new SpawnError( + errorType.command, + errorType.execPath, + errorType.error + ); + } + return; + } +} +export enum EXEC_TYPE { + SPAWN = "SPAWN", + EXEC = "EXEC", +} +export enum EXEC_EVENT { + PROGRESS = "PROGRESS", + ERROR = "ERROR", + END = "END", +} diff --git a/server/src/core/model/executor_result.ts b/server/src/core/model/executor_result.ts new file mode 100644 index 0000000..d3c632f --- /dev/null +++ b/server/src/core/model/executor_result.ts @@ -0,0 +1,19 @@ +import { EXEC_EVENT, EXEC_TYPE } from "./exec_error_model.js"; + + +export class ExecutorResult { + type: EXEC_TYPE; + event: EXEC_EVENT; + data: any; + constructor(type: EXEC_TYPE, event: EXEC_EVENT, data: any) { + this.type = type; + this.event = event; + this.data = data; + } + static isExecutorResult(value: any): void | ExecutorResult { + if ("type" in value && "event" in value && "data" in value) { + return new ExecutorResult(value.type, value.event, value.data); + } + return; + } + } \ No newline at end of file diff --git a/server/src/core/model/meta_data_file_manager_model.ts b/server/src/core/model/meta_data_file_manager_model.ts new file mode 100644 index 0000000..21097c5 --- /dev/null +++ b/server/src/core/model/meta_data_file_manager_model.ts @@ -0,0 +1,29 @@ +export enum EventsFileChanger { + remove = "remove", + update = "update", + delete = "delete", + create = "create", + static = "static", +} + +export class MetaDataFileManagerModel { + path: string; + md5Hash: string | null; + event: EventsFileChanger; + unixTime: number; + + constructor(path: string, md5Hash: string | null, event: EventsFileChanger) { + this.path = path; + this.md5Hash = md5Hash; + this.event = event; + this.unixTime = Date.now(); + } + public get timeString(): string { + const date = new Date(this.unixTime * 1000); + const hours = date.getHours(); + const minutes = "0" + date.getMinutes(); + const seconds = "0" + date.getSeconds(); + + return hours + ":" + minutes.substr(-2) + ":" + seconds.substr(-2); + } +} diff --git a/server/src/core/model/process_model.ts b/server/src/core/model/process_model.ts new file mode 100644 index 0000000..474d82a --- /dev/null +++ b/server/src/core/model/process_model.ts @@ -0,0 +1,45 @@ +export class CalculationProcess { + process: ProcessMetaData[]; + constructor(process: ProcessMetaData[]) { + this.process = process; + } +} + +export interface ProcessMetaData { + process: Process; + trigger: Trigger; + env: Env | null; +} + +export enum ProcessType { + EXEC = "EXEC", + SPAWN = "SPAWN", +} +export interface Env { + ssh_key: string; + isUserInput: boolean; + isExtends: string; +} + +export interface Process { + type: ProcessType; + command: string; + isGenerating: boolean; + isLocaleCode: boolean; + issueType: IssueType; + timeout: number; +} + +export enum IssueType { + WARNING = "WARNING", + ERROR = "ERROR", +} + +export enum TriggerType { + PROCESS = "PROCESS", + FILE = "FILE", +} +export interface Trigger { + type: TriggerType; + value: string; +} diff --git a/server/src/core/services/executor_program_service.ts b/server/src/core/services/executor_program_service.ts new file mode 100644 index 0000000..00dfe30 --- /dev/null +++ b/server/src/core/services/executor_program_service.ts @@ -0,0 +1,82 @@ +import cluster, { Worker } from "node:cluster"; +import { TypedEvent } from "../helper/typed_event.js"; +import { Result } from "../helper/result.js"; +import { WorkerDataExec, WorkerType } from "../helper/worker_computed.js"; +import { delay } from "../helper/delay.js"; +import { ExecutorResult } from "../model/executor_result.js"; +import { EXEC_TYPE, ExecError, SpawnError } from "../model/exec_error_model.js"; + +abstract class IExecutorProgramService { + abstract execPath: string; +} + +export class ExecutorProgramService + extends TypedEvent> + implements IExecutorProgramService +{ + static event = "ExecutorProgramService"; + execPath: string; + worker: Worker | any; + maxTime: null | number = null; + + constructor(execPath: string, maxTime: number | null = null) { + super(); + this.execPath = execPath; + this.maxTime = maxTime; + } + private async workerExecuted(command: string, workerType: WorkerType,args:Array | undefined = undefined) { + cluster.setupPrimary({ + exec: "./src/core/helper/worker_computed.js", + }); + + const worker = cluster.fork(); + + await delay(300); + + this.worker = worker; + + const workerDataExec: WorkerDataExec = { + command: command, + execPath: this.execPath, + type: workerType, + cliArgs:args + }; + worker.send(workerDataExec); + worker.on("message", (e) => { + const spawnError = SpawnError.isError(e); + if (spawnError instanceof SpawnError) { + this.emit(Result.error(spawnError)); + } + const executorResult = ExecutorResult.isExecutorResult(e); + if (executorResult instanceof ExecutorResult) { + this.emit(Result.ok(executorResult)); + } + + const execError = ExecError.isExecError(e); + if (execError instanceof ExecError) { + this.emit(Result.error(execError)); + } + }); + if (this.maxTime != null) { + setTimeout(() => { + this.worker.kill(); + this.emit( + Result.error( + WorkerType.EXEC + ? new ExecError(command, "timeout err") + : new SpawnError(command, "timeout err") + ) + ); + }, this.maxTime!); + } + } + public async call(type: EXEC_TYPE, command: string, args:Array | undefined = undefined): Promise { + if (type == EXEC_TYPE.EXEC) { + this.workerExecuted(command, WorkerType.EXEC); + + return; + } + this.workerExecuted(command, WorkerType.SPAWN,args); + return; + } +} diff --git a/server/src/core/services/files_change_notifier_service.ts b/server/src/core/services/files_change_notifier_service.ts new file mode 100644 index 0000000..ba6280f --- /dev/null +++ b/server/src/core/services/files_change_notifier_service.ts @@ -0,0 +1,122 @@ +import * as fs from "fs"; +import { resolve } from "node:path"; +import { promisify } from "node:util"; +import { createHash } from "node:crypto"; +import "reflect-metadata"; +import { BinaryLike } from "crypto"; +import { EventsFileChanger, MetaDataFileManagerModel } from "../model/meta_data_file_manager_model.js"; +import { Result } from "../helper/result.js"; +import { TypedEvent } from "../helper/typed_event.js"; + +const readFileAsync = promisify(fs.readFile); +const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat); + +function md5(content: Buffer | BinaryLike) { + return createHash("md5").update(content).digest("hex"); +} + +interface IHashesCache { + [key: string]: MetaDataFileManagerModel; +} +export abstract class IFilesChangeNotifierService { + abstract directory: string; +} +export class FilesChangeNotifierService + extends TypedEvent> + implements IFilesChangeNotifierService +{ + watcher!: fs.FSWatcher; + directory: string; + constructor(directory: string) { + super(); + this.directory = directory; + this.init(); + } + hashes: IHashesCache = {}; + async init() { + const files = await this.getFiles(this.directory); + for (const file of files) { + await this.setHash(file); + } + } + async setHash(file: string) { + const data = await readFileAsync(file); + const md5Current = md5(data); + + this.hashes[file] = new MetaDataFileManagerModel( + file, + md5Current, + EventsFileChanger.static + ); + this.emit(Result.ok(this.hashes)); + } + async getFiles(dir: string): Promise { + const subdirs = await readdir(dir); + const files = await Promise.all( + subdirs.map(async (subdir) => { + const res = resolve(dir, subdir); + return (await stat(res)).isDirectory() ? this.getFiles(res) : res; + }) + ); + return files.reduce((a: string | any[], f: any) => a.concat(f), []); + } + + public call = (): Result => { + try { + let md5Previous: string | null = null; + let fsWait: NodeJS.Timeout | boolean = false; + const watcher = fs.watch( + this.directory, + { recursive: true }, + async (_e, filename) => { + const filePath = this.directory + filename; + if (filename) { + if (fsWait) return; + fsWait = setTimeout(() => { + fsWait = false; + }, 100); + try { + const file = await readFileAsync(filePath); + const md5Current = md5(file); + + if (md5Current === md5Previous) { + return; + } + const status = this.hashes[filePath] === undefined + ? EventsFileChanger.create + : EventsFileChanger.update; + + const model = new MetaDataFileManagerModel( + filePath, + md5Current, + status + ); + this.hashes[filePath] = model; + md5Previous = md5Current; + this.emit(Result.ok(this.hashes)); + } catch (error) { + this.emit(Result.ok(this.hashes)); + this.hashes[filePath] = new MetaDataFileManagerModel( + filePath, + null, + EventsFileChanger.delete + ); + } + } + } + ); + this.watcher = watcher; + return Result.ok(true); + } catch (error) { + return Result.error(error as Error); + } + }; + cancel() { + if (this.watcher != undefined) { + this.watcher.close(); + this.listenersOncer = [] + } + + } +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..5f247c2 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/server/test/context/test.txt b/server/test/context/test.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/test/core/test_core.ts b/server/test/core/test_core.ts new file mode 100644 index 0000000..a20f87c --- /dev/null +++ b/server/test/core/test_core.ts @@ -0,0 +1,72 @@ +import { delay } from "../../src/core/helper/delay.js"; +import { Result } from "../../src/core/helper/result"; +import { TypedEvent } from "../../src/core/helper/typed_event.js"; + +export class TestCore { + allTests = 0; + testErr = 0; + testOk = 0; + private static _instance: TestCore; + + public static get instance() { + return this._instance || (this._instance = new this()); + } + + assert = (test: boolean, testName: string) => { + this.allTests += 1; + if (test) { + console.log("\x1b[32m", "✅ - " + testName); + this.testOk += 1; + return; + } + this.testErr += 1; + console.log("\x1b[31m", "❌ - " + testName); + }; + + testResult = () => { + console.log("\x1b[32m", "============="); + + if (this.allTests - this.testOk === 0) { + console.log( + "\x1b[32m", + `✅ All test success! ${this.allTests}/${this.testOk}` + ); + return; + } + if (this.testErr !== 0) { + console.log("\x1b[31m", "❌ test error:" + String(this.testErr)); + console.log("\x1b[32m", `✅ test success! ${this.testOk}`); + } + }; + resultTest = async ( + eventClass: TypedEvent> | any, + args: any, + testName: string, + isOk: boolean, + delayTime = 1000 + ) => { + let testIsOk = false; + eventClass.call(...args); + const listener = eventClass.on( + (e: { + fold: (arg0: (_s: any) => void, arg1: (_e: any) => void) => void; + }) => { + e.fold( + () => { + if (isOk) { + testIsOk = true; + } + }, + () => { + if (!isOk) { + testIsOk = true; + } + } + ); + } + ); + await delay(delayTime); + this.assert(testIsOk, testName); + listener.dispose(); + }; +} diff --git a/server/test/executor_program_service_test.ts b/server/test/executor_program_service_test.ts new file mode 100644 index 0000000..f1156e2 --- /dev/null +++ b/server/test/executor_program_service_test.ts @@ -0,0 +1,96 @@ +import { delay } from "../src/core/helper/delay.js"; +import { EXEC_TYPE } from "../src/core/model/exec_error_model.js"; +import { ExecutorResult } from "../src/core/model/executor_result.js"; +import { ExecutorProgramService } from "../src/core/services/executor_program_service.js"; +import { TestCore } from "./core/test_core.js"; +import { resultTest as resultTest, __dirname } from "./test.js"; +import { Worker } from "node:cluster"; + + + +export class ExecutorProgramServiceTest extends ExecutorProgramService { + timeCancel = 1000; + public test = async () => { + await this.resultsTests(); + await this.longTimeCancelTest() + await this.logWriteAndEventEndTestTypeExec() + await this.logWriteAndEventEndTypeSpawn() + }; + private async logWriteAndEventEndTypeSpawn(){ + const executorProgramService = await new ExecutorProgramService(__dirname + '/') + executorProgramService.call(EXEC_TYPE.SPAWN, 'node',['./mocks/log_code.js']) + const test = TestCore.instance + let testIsOk = false + let logEvent = false + + executorProgramService.on((e) =>{ + if(e.isSuccess()) { + const executorResult = e.value as ExecutorResult + if(logEvent == false){ + logEvent = executorResult.data != null && executorResult.data != undefined + } + testIsOk = executorResult.event == 'END' && logEvent + } + }) + await delay(8000) + test.assert(testIsOk,'ExecutorProgramService EXEC_TYPE.SPAWN end event and log write') + } + private async logWriteAndEventEndTestTypeExec(){ + const executorProgramService = await new ExecutorProgramService(__dirname) + executorProgramService.call(EXEC_TYPE.EXEC, 'node ./test/mocks/log_code.js' ) + const test = TestCore.instance + executorProgramService.on((e) =>{ + if(e.isSuccess()) { + const executorResult = e.value as ExecutorResult + test.assert(executorResult.data != undefined && executorResult.event == 'END','ExecutorProgramService EXEC_TYPE.EXEC end event and log write') + } + }) + await delay(7000) + } + private async longTimeCancelTest(){ + const executorProgramService = await new ExecutorProgramService('',1000) + executorProgramService.call(EXEC_TYPE.EXEC, 'node ./test/mocks/long_code.js' ) + await delay(1500) + const worker = executorProgramService.worker as Worker + const test = TestCore.instance + test.assert(worker.isDead(),'ExecutorProgramService long time cancel') + + } + private resultsTests = async () => { + await resultTest( + new ExecutorProgramService(__dirname), + [EXEC_TYPE.EXEC, "node ./mocks/error.js"], + "ExecutorProgramService EXEC_TYPE.EXEC on Result.error", + false, + 2000 + ); + await delay(400) + await resultTest( + new ExecutorProgramService(__dirname), + [EXEC_TYPE.EXEC, "ls"], + "ExecutorProgramService EXEC_TYPE.EXEC on Result.ok", + true + ); + + await resultTest( + new ExecutorProgramService(__dirname), + [EXEC_TYPE.SPAWN, "ls"], + "ExecutorProgramService EXEC_TYPE.SPAWN on Result.ok", + true + ); + await resultTest( + new ExecutorProgramService(__dirname), + [EXEC_TYPE.SPAWN, "python3 ./mocks/s.js"], + "ExecutorProgramService EXEC_TYPE.SPAWN on Result.error", + false, + 2000 + ); + await resultTest( + new ExecutorProgramService(__dirname), + [EXEC_TYPE.SPAWN, "ls"], + "ExecutorProgramService EXEC_TYPE.SPAWN on Result.ok", + true, + 2000 + ); + }; +} diff --git a/server/test/files_change_notifier_service_test.ts b/server/test/files_change_notifier_service_test.ts new file mode 100644 index 0000000..0a15b16 --- /dev/null +++ b/server/test/files_change_notifier_service_test.ts @@ -0,0 +1,86 @@ +import { delay } from "../src/core/helper/delay.js"; +import { EventsFileChanger } from "../src/core/model/meta_data_file_manager_model.js"; +import { FilesChangeNotifierService } from "../src/core/services/files_change_notifier_service.js"; +import { assert, __dirname } from "./test.js"; +import * as fs from "fs"; + +export class FilesChangerTest extends FilesChangeNotifierService { + directory = __dirname + "/context/"; + data = () => { + return "This is a file containing a collection"; + }; + filePath = this.directory + "programming.txt"; + + public hashUnitEqualTo(hashEqualValue: EventsFileChanger, testName: string) { + let testIsOk = false; + + for (const [_key, value] of Object.entries(this.hashes)) { + if ((value.event === hashEqualValue, testName)) { + testIsOk = true; + } + } + assert(testIsOk, testName); + } + public async createFile() { + this.call(); + await delay(2000); + fs.writeFileSync(this.filePath, this.data()); + await delay(1000); + this.hashUnitEqualTo( + EventsFileChanger.create, + "FilesChangeNotifierService create file" + ); + + this.cancel(); + } + public async updateFile() { + this.call(); + fs.writeFileSync(this.filePath, this.data() + "132321"); + await delay(1000); + fs.writeFileSync(this.filePath, this.data() + "132"); + await delay(500); + this.hashUnitEqualTo( + EventsFileChanger.update, + "FilesChangeNotifierService update file" + ); + this.cancel(); + } + public async initFile() { + this.init(); + await delay(500); + this.hashUnitEqualTo( + EventsFileChanger.static, + "FilesChangeNotifierService init file" + ); + } + public async deleteFile() { + this.call(); + fs.unlinkSync(this.filePath); + await delay(1000); + this.hashUnitEqualTo( + EventsFileChanger.delete, + "FilesChangeNotifierService delete file" + ); + this.cancel(); + } + public async notExistsDirectory() { + await delay(1000); + this.directory = ""; + const result = this.call(); + assert(result.isFailure(), "Not exists directory"); + this.directory = __dirname + "/context/"; + } + public async test() { + await this.createFile(); + await this.updateFile() + await this.initFile() + await this.deleteFile() + await this.notExistsDirectory() + await this.testClear(); + } + public testClear() { + if (fs.existsSync(this.filePath)) { + fs.unlinkSync(this.filePath); + } + } +} diff --git a/server/test/mocks/log_code.ts b/server/test/mocks/log_code.ts new file mode 100644 index 0000000..2ab9c3c --- /dev/null +++ b/server/test/mocks/log_code.ts @@ -0,0 +1,17 @@ + + +setTimeout(() =>{ + console.log('log') + console.log('log') + console.log('log') + console.log('log') + console.log('log') + console.log('log') + console.log('log') + console.log('log') + console.log('log') + console.log('log') +},2000) +setTimeout(() =>{ + console.log('log end') +}, 5000) \ No newline at end of file diff --git a/server/test/mocks/long_code.ts b/server/test/mocks/long_code.ts new file mode 100644 index 0000000..d257418 --- /dev/null +++ b/server/test/mocks/long_code.ts @@ -0,0 +1,4 @@ +const seconds = 1000 * 10 +setTimeout(()=>{ + console.log(200) +}, seconds) \ No newline at end of file diff --git a/server/test/test.ts b/server/test/test.ts new file mode 100644 index 0000000..f7886d7 --- /dev/null +++ b/server/test/test.ts @@ -0,0 +1,47 @@ +import locator from "../src/core/di/register_di.js"; +import { UnitTestEnv } from "../src/core/di/env.js"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; +import { ExecutorProgramServiceTest } from "./executor_program_service_test.js"; +import { FilesChangerTest } from "./files_change_notifier_service_test.js"; +import { TestCore } from "./core/test_core.js"; +import "reflect-metadata"; +import { CalculationProcess, IssueType, ProcessMetaData, ProcessType, TriggerType } from "../src/core/model/process_model.js"; + +const testCore = TestCore.instance; +const __filename: string = fileURLToPath(import.meta.url); + +export const __dirname: string = dirname(__filename); +export const assert = testCore.assert; +export const resultTest = testCore.resultTest; +const env = new UnitTestEnv(__dirname); + +locator(env); + +const main = async () => { + await new ExecutorProgramServiceTest(__dirname).test(); + await new FilesChangerTest(__dirname).test() + await testCore.testResult(); +}; + +main(); +// const logProcess:ProcessMetaData = { +// process: { +// type: ProcessType.EXEC, +// command: `nix-shell -p 'python39.withPackages(ps: with ps; [ ])' --run 'python3 p.py 1'`, +// isGenerating: false, +// isLocaleCode: false, +// issueType: IssueType.WARNING, +// timeout: 10000000, +// }, + +// trigger: { +// type: TriggerType.PROCESS, +// value: 'code', +// }, +// env: null, + +// } +// const calculationProcess = new CalculationProcess([]) + + \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..33bce8f --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "esnext", + "dom" + ], + "moduleResolution":"node", + "sourceMap": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "pretty": true, + "strict": true, + "skipLibCheck": true, + "noImplicitAny": false + // "moduleResolution": "node" + }, + // "include": ["src/**/*.ts", "src/**/*.json", ".env"], + // "exclude": ["node_modules"] + +} \ No newline at end of file diff --git a/server/tslint.json b/server/tslint.json new file mode 100644 index 0000000..ccc3348 --- /dev/null +++ b/server/tslint.json @@ -0,0 +1,24 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "max-classes-per-file": false, + "max-line-length": false, + "no-empty": false, + "only-arrow-functions": false, + "no-console": false, + "object-literal-sort-keys": false, + "interface-name": false, + "ban-types": false, + "no-shadowed-variable": false, + "forin": false, + "member-ordering": false, + "deprecation": true, + "no-reference": false, + "trailing-comma": false + }, + "rulesDirectory": [] +}