diff --git a/.DS_Store b/.DS_Store index f7210bb..fd6d5ac 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.vscode/launch.json b/.vscode/launch.json index a34174e..e477ca7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,6 +25,16 @@ "run-script", "test:watch" ], "cwd": "${workspaceRoot}/server/", + }, + { + "type": "node", + "request": "launch", + "name": "ui-dev", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run-script", "dev" + ], + "cwd": "${workspaceRoot}/ui/", } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 97c3e4c..da63cc5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,20 @@ { "files.exclude": { - "server/build/src/core/*controllers.*": true, - "server/build/src/core/*di": true, - "server/build/src/core/*di.*": true, - "server/build/src/core/*extensions": true, - "server/build/src/core/*extensions.*": true, - "server/build/src/core/*helper": true, - "server/build/src/core/*helper.*": true, - "server/build/src/core/*interfaces": true, - "server/build/src/core/*interfaces.*": true, - "server/build/src/core/*middlewares": true, - "server/build/src/core/*middlewares.*": true, - "server/build/src/core/*model": true, - "server/build/src/core/*model.*": true, - "server/build/src/core/*services": true, - "server/build/src/core/*services.*": true, - "server/build/src/core/*usecases": true, - "server/build/src/core/*usecases.*": true, - "server/src/core/model/exec_error_model.js": true, - "**/*.map": true, - "**/*.js": true - } -} \ No newline at end of file + "**/.git": false, + "**/.svn": false, + "**/.hg": false, + "**/CVS": false, + "**/__pycache__": false, + "*.DS_Store": false, + "*.DS_Store.*": false, + "*.git": false, + "*.git.*": false, + "*.vscode": false, + "*.vscode.*": false, + "*server.*": false, + "*ui": false, + "*ui.*": false + }, + "cSpell.words": ["antd", "fileupload", "metadatas", "undici", "uuidv"], + "editor.rulers": [100] +} diff --git a/server/.gitignore b/server/.gitignore index 64a820b..10894c2 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -7,4 +7,5 @@ coverage package-lock.json .*.swp build/ -model_create.ts \ No newline at end of file +model_create.ts +public diff --git a/server/package.json b/server/package.json index 868174c..a0cd0d3 100644 --- a/server/package.json +++ b/server/package.json @@ -5,9 +5,9 @@ "main": "index.js", "scripts": { "pretest": "tsc", - "test": "ts-node ./build/test/test.js", - "test:watch": "tsc-watch --onSuccess 'ts-node ./build/test/test.js'", - "dev": "tsc-watch --onSuccess 'ts-node ./build/src/main.js'" + "test:unit": "NODE_ENV=unit tsc-watch --onSuccess 'ts-node ./build/test/test.js'", + "test:e2e": "NODE_ENV=e2e tsc-watch --onSuccess 'ts-node ./build/test/test.js'", + "dev": "NODE_ENV=dev tsc-watch --onSuccess 'ts-node ./build/src/main.js'" }, "author": "IDONTSUDO", "devDependencies": { @@ -31,12 +31,13 @@ }, "dependencies": { "@grpc/grpc-js": "^1.9.0", + "axios": "^1.6.2", "babel-register": "^6.26.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "concurrently": "^8.2.0", "cors": "^2.8.5", "express": "^4.18.2", + "express-fileupload": "^1.4.2", "first-di": "^1.0.11", "md5": "^2.3.0", "mongoose": "^7.6.2", @@ -49,6 +50,6 @@ "spark-md5": "^3.0.2", "ts-md5": "^1.3.1", "tsc-watch": "^6.0.4", - "typedi": "^0.10.0" + "uuid": "^9.0.1" } } diff --git a/server/src/core/controllers/app.ts b/server/src/core/controllers/app.ts index d868669..05ca80b 100644 --- a/server/src/core/controllers/app.ts +++ b/server/src/core/controllers/app.ts @@ -1,32 +1,67 @@ import express from "express"; import { Routes } from "../interfaces/router"; import cors from "cors"; -import locator from "../di/register_di"; -import { DevEnv, UnitTestEnv } from "../di/env"; -import mongoose from "mongoose"; -import http from "http"; -// import { Server } from "socket.io"; +import { Server } from "socket.io"; +import { createServer } from "http"; +import { SocketSubscriber } from "./socket_controller"; +import { dirname } from "path"; +import fileUpload from "express-fileupload"; +import { SetLastActivePipelineToRealTimeServiceScenario } from "../scenarios/set_active_pipeline_to_realtime_service_scenario"; +import { CheckAndCreateStaticFilesFolderUseCase } from "../usecases/check_and_create_static_files_folder_usecase"; +import { DataBaseConnectUseCase } from "../usecases/database_connect_usecase"; +import { TypedEvent } from "../helpers/typed_event"; -export class App { +export enum ServerStatus { + init = "init", + finished = "finshed", + error = "error", +} +export enum Environment { + DEV = "DEV", + E2E_TEST = "E2E_TEST", +} + +export class App extends TypedEvent { public app: express.Application; public port: number; - public env: string; - public computedFolder: string; - // public io: - constructor(routes: Routes[], computedFolder: string) { - this.port = 3000; - this.env = "dev"; - this.loadAppDependencies().then(() => { - this.app = express(); - this.initializeMiddlewares(); - this.initializeRoutes(routes); - this.computedFolder = computedFolder; - }); + public env: Environment; + public socketSubscribers: SocketSubscriber[]; + public io: Server; + status: ServerStatus; + + constructor(routes: Routes[] = [], socketSubscribers: SocketSubscriber[] = [], env = Environment.DEV) { + super(); + this.init(routes, socketSubscribers, env); } + public init(routes: Routes[], socketSubscribers: SocketSubscriber[], env: Environment) { + this.port = 4001; + this.socketSubscribers = socketSubscribers; + this.env = env; + this.app = express(); + this.setServerStatus(ServerStatus.init); + + this.loadAppDependencies().then(() => { + this.initializeMiddlewares(); + this.initializeRoutes(routes); + if (this.status !== ServerStatus.error) { + this.setServerStatus(ServerStatus.finished); + } + }); + } public listen() { - const httpServer = new http.Server(this.app); - // const io = new Server(httpServer); + const httpServer = createServer(this.app); + const io = new Server(httpServer, { + cors: { origin: "*" }, + }); + + io.on("connection", (socket) => { + this.socketSubscribers.map((el) => { + el.emitter.on((e) => { + socket.emit(el.event, e); + }); + }); + }); httpServer.listen(this.port, () => { console.info(`=================================`); @@ -35,18 +70,13 @@ export class App { console.info(`🚀 WS ws://localhost:${this.port}`); console.info(`=================================`); }); - // io.on("connection", (socket) => { - // socket.on("disconnect", function (msg) { - // console.log("Disconnected"); - // }); - // }); - // setInterval(function () { - // io.emit("goodbye"); - // console.log(200); - // }, 1000); + this.io = io; + } + setServerStatus(status: ServerStatus) { + this.emit(status); + this.status = status; } - public getServer() { return this.app; } @@ -55,6 +85,13 @@ export class App { this.app.use(cors()); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); + this.app.use(express.static("public")); + + this.app.use( + fileUpload({ + createParentPath: true, + }) + ); } private initializeRoutes(routes: Routes[]) { @@ -62,21 +99,25 @@ export class App { this.app.use("/", route.router); }); } - async loadAppDependencies() { - await locator( - this.env == "development" - ? new DevEnv(this.computedFolder) - : new UnitTestEnv(this.computedFolder) + + async loadAppDependencies(): Promise { + const dataBaseName = this.env === Environment.E2E_TEST ? "e2e_test" : "dev"; + // TODO(IDONTSUDO):maybe convert it to a class and map it there + const result = await new DataBaseConnectUseCase().call(dataBaseName); + await result.fold( + async (_s) => { + await new CheckAndCreateStaticFilesFolderUseCase().call(); + await new SetLastActivePipelineToRealTimeServiceScenario().call(); + }, + async (_e) => { + this.setServerStatus(ServerStatus.error); + } ); - - mongoose - .connect("mongodb://127.0.0.1:27017/test") - .then(() => console.log("Connected!")) - .catch((e) => { - console.log("ERROR:", e); - }); } + + static staticFilesStoreDir = () => { + const dir = dirname(__filename); + const rootDir = dir.slice(0, dir.length - 20); + return rootDir + "public/"; + }; } - - - \ No newline at end of file diff --git a/server/src/core/controllers/crud_controller.ts b/server/src/core/controllers/crud_controller.ts index d2ebacc..c0b73e6 100644 --- a/server/src/core/controllers/crud_controller.ts +++ b/server/src/core/controllers/crud_controller.ts @@ -12,23 +12,23 @@ export class CrudController extends CoreHttpController { constructor(routerModel: IRouteModel) { super(routerModel); - this.url = "/" + routerModel.url; + this.mainURL = "/" + routerModel.url; this.validationModel = routerModel.validationModel; this.dataBaseModel = routerModel.databaseModel; this.init(); } init() { - this.routes["POST"] = new CreateDataBaseModelUseCase( - this.dataBaseModel - ).call; - this.routes["GET"] = new PaginationDataBaseModelUseCase( - this.dataBaseModel - ).call; - this.routes["DELETE"] = new DeleteDataBaseModelUseCase( - this.dataBaseModel - ).call; - this.routes["PUT"] = new UpdateDataBaseModelUseCase( - this.dataBaseModel - ).call; + if (this.routes["POST"] === null) { + this.routes["POST"] = new CreateDataBaseModelUseCase(this.dataBaseModel).call; + } + if (this.routes["GET"] === null) { + this.routes["GET"] = new PaginationDataBaseModelUseCase(this.dataBaseModel).call; + } + if (this.routes["DELETE"] === null) { + this.routes["DELETE"] = new DeleteDataBaseModelUseCase(this.dataBaseModel).call; + } + if (this.routes["PUT"] === null) { + this.routes["PUT"] = new UpdateDataBaseModelUseCase(this.dataBaseModel).call; + } } } diff --git a/server/src/core/controllers/http_controller.ts b/server/src/core/controllers/http_controller.ts index 55fbce9..ae3b30d 100644 --- a/server/src/core/controllers/http_controller.ts +++ b/server/src/core/controllers/http_controller.ts @@ -1,19 +1,54 @@ import { validationModelMiddleware } from "../middlewares/validation_model"; -import { Result } from "../helper/result"; +import { Result } from "../helpers/result"; import { Router, Request, Response } from "express"; import { IRouteModel, Routes } from "../interfaces/router"; -export type CallBackFunction = (a: T) => Promise>; +export type HttpMethodType = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "PATCH" | "HEAD"; + +export type ResponseBase = Promise>; + +export abstract class CallbackStrategyWithEmpty { + abstract call(): ResponseBase; +} +export abstract class CallbackStrategyWithValidationModel { + abstract validationModel: V; + abstract call(a: V): ResponseBase; +} +export abstract class CallbackStrategyWithIdQuery { + abstract idValidationExpression: RegExp | null; + abstract call(id: string): ResponseBase; +} +export abstract class CallBackStrategyWithQueryPage { + abstract validationPageExpression: RegExp | null; + abstract call(page: string): ResponseBase; +} + +export abstract class CallbackStrategyWithFileUpload { + abstract checkingFileExpression: RegExp; + abstract call(file: File): ResponseBase; +} + +interface ISubSetFeatureRouter { + method: HttpMethodType; + subUrl: string; + fn: + | CallbackStrategyWithValidationModel + | CallbackStrategyWithEmpty + | CallbackStrategyWithIdQuery + | CallBackStrategyWithQueryPage + | CallbackStrategyWithFileUpload; +} abstract class ICoreHttpController { - abstract url: string; + abstract mainURL: string; public router = Router(); abstract call(): Routes; } export class CoreHttpController implements ICoreHttpController { - url: string; + mainURL: string; validationModel: any; + subRoutes: ISubSetFeatureRouter[] = []; routes = { POST: null, @@ -25,55 +60,97 @@ export class CoreHttpController implements ICoreHttpController { public router = Router(); constructor(routerModel: IRouteModel) { - this.url = "/" + routerModel.url; + this.mainURL = "/" + routerModel.url; this.validationModel = routerModel.validationModel; } - + async responseHelper(res: Response, fn: ResponseBase) { + (await fn).fold( + (ok) => { + res.json(ok); + return; + }, + (err) => { + res.status(400).json({ error: String(err) }); + return; + } + ); + } call(): Routes { + if (this.subRoutes.isNotEmpty()) { + this.subRoutes.map((el) => { + this.router[el.method.toLowerCase()](this.mainURL + "/" + el.subUrl, async (req, res) => { + if (el.fn instanceof CallbackStrategyWithValidationModel) { + // TODO(IDONTSUDO): + throw Error("needs to be implimed"); + } + if (el.fn instanceof CallbackStrategyWithIdQuery) { + throw Error("needs to be implimed"); + } + if (el.fn instanceof CallBackStrategyWithQueryPage) { + throw Error("needs to be implimed"); + } + if (el.fn instanceof CallbackStrategyWithEmpty) { + await this.responseHelper(res, el.fn.call()); + return; + } + + if (el.fn instanceof CallbackStrategyWithFileUpload) { + if (req["files"] === undefined) { + res.status(400).json("need files to form-data request"); + return; + } + if (req["files"]["file"] === undefined) { + res.status(400).json("need file to form data request"); + return; + } + if (el.fn instanceof CallbackStrategyWithFileUpload) { + if (!el.fn.checkingFileExpression.test(req["files"]["file"]["name"])) { + res.status(400).json("a file with this extension is expected: " + String(el.fn.checkingFileExpression)); + return; + } + } + await this.responseHelper(res, el.fn.call(req["files"]["file"])); + } + }); + }); + } + if (this.routes["POST"] != null) { - this.router.post( - this.url, - validationModelMiddleware(this.validationModel), - (req, res) => - this.requestResponseController(req, res, this.routes["POST"]) + this.router.post(this.mainURL, validationModelMiddleware(this.validationModel), (req, res) => + this.requestResponseController(req, res, this.routes["POST"]) ); } if (this.routes["DELETE"] != null) { - this.router.delete(this.url, (req, res) => + this.router.delete(this.mainURL, (req, res) => this.requestResponseController(req, res, this.routes["DELETE"]) ); } if (this.routes["PUT"] != null) { - this.router.put( - this.url, - validationModelMiddleware(this.validationModel), - (req, res) => - this.requestResponseController(req, res, this.routes["PUT"]) + this.router.put(this.mainURL, validationModelMiddleware(this.validationModel), (req, res) => + this.requestResponseController(req, res, this.routes["PUT"]) ); } if (this.routes["GET"] != null) { - this.router.get(this.url, (req, res) => - this.requestResponseController(req, res, this.routes["GET"]) - ); + this.router.get(this.mainURL, (req, res) => this.requestResponseController(req, res, this.routes["GET"])); } return { router: this.router, }; } - public put(usecase: CallBackFunction) { + public put(usecase: CallbackStrategyWithValidationModel) { this.routes["PUT"] = usecase; } - public delete(usecase: CallBackFunction) { + public delete(usecase: CallbackStrategyWithValidationModel) { this.routes["DELETE"] = usecase; } - private async requestResponseController( + public async requestResponseController( req: Request, res: Response, - usecase: CallBackFunction + usecase: CallbackStrategyWithValidationModel ) { let payload = null; - + const useCase = usecase as any; if (req["model"] != undefined) { payload = req.body as T; } @@ -85,23 +162,23 @@ export class CoreHttpController implements ICoreHttpController { if (req.query.id !== undefined) { payload = String(req.query.id); } - - (await usecase(payload)).fold( + (await useCase(payload)).fold( (ok) => { res.json(ok); return; }, (err) => { - res.status(400).json(err); + res.status(400).json({ error: String(err) }); return; } ); } - public post(usecase: CallBackFunction) { + // TODO(IDONTSUDO):need fix to CallbackStrategyWithValidationModel + public post(usecase: any) { this.routes["POST"] = usecase; } - public get(usecase: CallBackFunction) { + public get(usecase: any) { this.routes["GET"] = usecase; } } diff --git a/server/src/core/controllers/routes.ts b/server/src/core/controllers/routes.ts new file mode 100644 index 0000000..76e8f90 --- /dev/null +++ b/server/src/core/controllers/routes.ts @@ -0,0 +1,26 @@ +import { NixStoreManagerPresentation } from "../../features/nix_store_manager/nix_store_manager"; +import { PipelinePresentation } from "../../features/pipelines/pipeline_presentation"; +import { ProcessPresentation } from "../../features/process/process_presentation"; +import { ProjectInstancePresentation } from "../../features/project_instance/project_instance_presentation"; +import { ProjectsPresentation } from "../../features/projects/projects_presentation"; +import { RealTimePresentation } from "../../features/realtime/realtime_presentation"; +import { TriggerPresentation } from "../../features/triggers/triggers_presentation"; +import { extensions } from "../extensions/extensions"; +import { Routes } from "../interfaces/router"; + +extensions(); + +export const routersImplementPureCrud = [ + new TriggerPresentation(), + new ProjectsPresentation(), + new ProcessPresentation(), + new PipelinePresentation(), +]; + +export const httpRoutes: Routes[] = [ + new RealTimePresentation(), + new ProjectInstancePresentation(), + new NixStoreManagerPresentation(), +] + .concat(routersImplementPureCrud) + .map((el) => el.call()); diff --git a/server/src/core/controllers/socket_controller.ts b/server/src/core/controllers/socket_controller.ts index 9e20248..59cb075 100644 --- a/server/src/core/controllers/socket_controller.ts +++ b/server/src/core/controllers/socket_controller.ts @@ -1,15 +1,10 @@ -// import path from "path"; -// import { TypedEvent } from "../helper/typed_event"; -// import { StackService } from "../services/stack_service"; -// // TODO(IDONTSUDO): up to do +import { TypedEvent } from "../helpers/typed_event"; -// class SocketController{ -// emitter:TypedEvent; -// constructor(emitter:TypedEvent, ){ -// this.emitter = emitter -// } -// call = () =>{ - -// } -// } - \ No newline at end of file +export class SocketSubscriber { + emitter: TypedEvent; + event: string; + constructor(emitter: TypedEvent, event: string) { + this.emitter = emitter; + this.event = event; + } +} diff --git a/server/src/core/di/env.ts b/server/src/core/di/env.ts index 55841e6..601a751 100644 --- a/server/src/core/di/env.ts +++ b/server/src/core/di/env.ts @@ -1,44 +1,44 @@ -import { Service } from "typedi"; +// import { Service } from "typedi"; -@Service() -export class IEnv{ - rootFolder!: string; - constructor(){ +// @Service() +// export class IEnv{ +// rootFolder!: string; +// constructor(){ - } - toStringEnv(){ - return '' - } - static env(){ - return '' - } -} +// } +// toStringEnv(){ +// return '' +// } +// static env(){ +// return '' +// } +// } -@Service() -export class DevEnv implements IEnv { - rootFolder:string; - constructor(rootFolder:string){ - this.rootFolder = rootFolder - } - toStringEnv(): string { - return DevEnv.env() - } - static env(){ - return 'DevEnv' +// @Service() +// export class DevEnv implements IEnv { +// rootFolder:string; +// constructor(rootFolder:string){ +// this.rootFolder = rootFolder +// } +// toStringEnv(): string { +// return DevEnv.env() +// } +// static env(){ +// return 'DevEnv' - } -} -@Service() -export class UnitTestEnv implements IEnv{ - rootFolder:string; - constructor(rootFolder:string){ - this.rootFolder = rootFolder - } - toStringEnv(): string { - return UnitTestEnv.env() - } - static env(){ - return 'UnitTestEnv' +// } +// } +// @Service() +// 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 index 77c6b6d..684bd08 100644 --- a/server/src/core/di/register_di.ts +++ b/server/src/core/di/register_di.ts @@ -1,53 +1,53 @@ -import { DevEnv, IEnv, UnitTestEnv } from "./env"; -import { extensions } from "../extensions/extensions"; -// import { Container, Service } from 'typedi'; +// import { DevEnv, IEnv, UnitTestEnv } from "./env"; +// import { extensions } from "../extensions/extensions"; +// // import { Container, Service } from 'typedi'; -export default function locator(env: IEnv) { - extensions(); - envRegister(env); - registerRepository(env); - registerController(env); - registerService(env); - // override(MetaDataFileManagerModel, MetaDataFileManagerModel); -} +// export default function locator(env: IEnv) { +// extensions(); +// 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 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); +// const registerRepository = (env: IEnv) => { +// switch (env.toStringEnv()) { +// case UnitTestEnv.env(): +// // override(IEnv, UnitTestEnv); - return; - case DevEnv.env(): - // override(IEnv, DevEnv); - return; - } -}; +// return; +// case DevEnv.env(): +// // override(IEnv, DevEnv); +// return; +// } +// }; -const registerController = (env: IEnv) => { - switch (env.toStringEnv()) { - case UnitTestEnv.env(): - return; - case DevEnv.env(): - 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; - } -}; +// const registerService = (env: IEnv) => { +// switch (env.toStringEnv()) { +// case UnitTestEnv.env(): +// return; +// case DevEnv.env(): +// return; +// } +// }; diff --git a/server/src/core/extensions/array.ts b/server/src/core/extensions/array.ts index 46e38d1..f62faa8 100644 --- a/server/src/core/extensions/array.ts +++ b/server/src/core/extensions/array.ts @@ -1,25 +1,23 @@ -export {}; - -declare global { - interface Array { - // @strict: The parameter is determined whether the arrays must be exactly the same in content and order of this relationship or simply follow the same requirements. - equals(array: Array, strict: boolean): boolean; +/* eslint-disable @typescript-eslint/no-this-alias */ +export const ArrayExtensions = () => { + if ([].firstElement === undefined) { + Array.prototype.firstElement = function () { + return this[0]; + }; } -} - -export const ArrayEquals = () => { - if ([].equals == undefined) { + if ([].equals === undefined) { + // eslint-disable-next-line no-extend-native Array.prototype.equals = function (array, strict = true) { if (!array) return false; - if (arguments.length == 1) strict = true; + if (arguments.length === 1) strict = true; - if (this.length != array.length) return false; + if (this.length !== array.length) return false; for (let i = 0; i < this.length; i++) { if (this[i] instanceof Array && array[i] instanceof Array) { if (!this[i].equals(array[i], strict)) return false; - } else if (strict && this[i] != array[i]) { + } else if (strict && this[i] !== array[i]) { return false; } else if (!strict) { return this.sort().equals(array.sort(), true); @@ -28,4 +26,27 @@ export const ArrayEquals = () => { return true; }; } + if ([].lastElement === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.lastElement = function () { + const instanceCheck = this; + if (instanceCheck === undefined) { + return undefined; + } else { + const instance = instanceCheck as []; + return instance[instance.length - 1]; + } + }; + } + if ([].isEmpty === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.isEmpty = function () { + return this.length === 0; + }; + } + if ([].isNotEmpty === undefined) { + Array.prototype.isNotEmpty = function () { + return this.length !== 0; + }; + } }; diff --git a/server/src/core/extensions/extensions.ts b/server/src/core/extensions/extensions.ts index c02556b..14e8907 100644 --- a/server/src/core/extensions/extensions.ts +++ b/server/src/core/extensions/extensions.ts @@ -1,6 +1,24 @@ -import { ArrayEquals } from "./array"; +import { ArrayExtensions } from "./array"; +import { StringExtensions } from "./string"; -export const extensions = () =>{ - ArrayEquals() +declare global { + interface Array { + // @strict: The parameter is determined whether the arrays must be exactly the same in content and order of this relationship or simply follow the same requirements. + equals(array: Array, strict: boolean): boolean; + lastElement(): T | undefined; + firstElement(): T | undefined; + isEmpty(): boolean; + isNotEmpty(): boolean; + } + interface String { + isEmpty(): boolean; + isNotEmpty(): boolean; + lastElement(): string; + hasPattern(pattern: string): boolean; + hasNoPattern(pattern: string): boolean; + } } - \ No newline at end of file +export const extensions = () => { + ArrayExtensions(); + StringExtensions(); +}; diff --git a/server/src/core/extensions/string.ts b/server/src/core/extensions/string.ts new file mode 100644 index 0000000..bdd093d --- /dev/null +++ b/server/src/core/extensions/string.ts @@ -0,0 +1,29 @@ +export const StringExtensions = () => { + if ("".isEmpty === undefined) { + // eslint-disable-next-line no-extend-native + String.prototype.isEmpty = function () { + return this.length === 0; + }; + } + if ("".isNotEmpty === undefined) { + // eslint-disable-next-line no-extend-native + String.prototype.isNotEmpty = function () { + return this.length !== 0; + }; + } + if ("".lastElement === undefined) { + String.prototype.lastElement = function () { + return this[this.length - 1]; + }; + } + if ("".hasPattern === undefined) { + String.prototype.hasPattern = function (pattern) { + return new RegExp(pattern).test(this); + }; + } + if ("".hasNoPattern === undefined) { + String.prototype.hasNoPattern = function (pattern) { + return !this.hasPattern(pattern); + }; + } +}; diff --git a/server/src/core/helper/cancelable_promise.ts b/server/src/core/helper/cancelable_promise.ts deleted file mode 100644 index 0519ecb..0000000 --- a/server/src/core/helper/cancelable_promise.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/server/src/core/helper/delay.ts b/server/src/core/helper/delay.ts deleted file mode 100644 index e502651..0000000 --- a/server/src/core/helper/delay.ts +++ /dev/null @@ -1,3 +0,0 @@ -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/typed_event.ts b/server/src/core/helper/typed_event.ts deleted file mode 100644 index d194064..0000000 --- a/server/src/core/helper/typed_event.ts +++ /dev/null @@ -1,49 +0,0 @@ -export interface Listener { - (event: T): any; -} - -export interface Disposable { - dispose():void; -} - - -export class TypedEvent { - private listeners: Listener[] = []; - public listenersOnces: Listener[] = []; - - on = (listener: Listener): Disposable => { - this.listeners.push(listener); - return { - dispose: () => this.off(listener), - }; - }; - - once = (listener: Listener): void => { - this.listenersOnces.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.listenersOnces.length > 0) { - const toCall = this.listenersOnces; - this.listenersOnces = []; - 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/helpers/class_validator_mocker.ts b/server/src/core/helpers/class_validator_mocker.ts new file mode 100644 index 0000000..433d17a --- /dev/null +++ b/server/src/core/helpers/class_validator_mocker.ts @@ -0,0 +1,85 @@ +import { randomBytes, randomInt, randomUUID } from "crypto"; +import { getMetadataStorage, IS_ARRAY, IS_BOOLEAN, IS_MONGO_ID, IS_NUMBER, IS_STRING, IS_UUID } from "class-validator"; +import { ValidationMetadata } from "class-validator/types/metadata/ValidationMetadata"; + +type AvailableTypes = string | number | boolean | undefined | []; + +export class ClassValidatorMocker { + // eslint-disable-next-line @typescript-eslint/ban-types + public static create(constructor: Function, partial: Partial = {}): T { + return new ClassValidatorMocker().create(constructor, partial); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + public create(constructor: Function, partial: Partial = {}): T { + const metadataStorage = getMetadataStorage(); + const targetMetadatas = metadataStorage.getTargetValidationMetadatas(constructor, "", false, false); + const groupedMetadatas = metadataStorage.groupByPropertyName(targetMetadatas); + // nestedValidation + let randomFixture = {} as T; + + for (const propertyName of Object.keys(groupedMetadatas)) { + const metadatas = groupedMetadatas[propertyName]; + const value = this.generatePropertyValueFromMetadatas(metadatas); + + if (value !== undefined) { + randomFixture = { + ...randomFixture, + [propertyName]: value, + }; + } + } + + return { ...randomFixture, ...partial }; + } + + private generatePropertyValueFromMetadatas(metadatas: ValidationMetadata[]): AvailableTypes { + for (const metadata of metadatas) { + const constraints = getMetadataStorage().getTargetValidatorConstraints(metadata.constraintCls); + + for (const constraint of constraints) { + switch (constraint.name) { + case IS_ARRAY: + return []; + case "isEnum": + return Object.keys(metadata.constraints.firstElement()).firstElement(); + case IS_MONGO_ID: + return this.randomUUID(); + case IS_STRING: + return this.randomString(); + case IS_NUMBER: + return this.randomNumber(); + case IS_BOOLEAN: + return this.randomBoolean(); + case IS_UUID: + return this.randomUUID(); + + default: + break; + } + } + } + + return undefined; + } + + private randomString(): string { + return randomBytes(randomInt(1, 10)).toString("hex"); + } + + private randomNumber(): number { + return randomInt(0, 99_999); + } + + private randomBoolean(): boolean { + return randomInt(0, 1) === 1; + } + + private randomUUID(): string { + if (randomUUID != null) { + return randomUUID(); + } + + return randomBytes(16).toString("hex"); + } +} diff --git a/server/src/core/helpers/delay.ts b/server/src/core/helpers/delay.ts new file mode 100644 index 0000000..98eaa4f --- /dev/null +++ b/server/src/core/helpers/delay.ts @@ -0,0 +1,3 @@ +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/server/src/core/helpers/result.ts b/server/src/core/helpers/result.ts new file mode 100644 index 0000000..dc689bf --- /dev/null +++ b/server/src/core/helpers/result.ts @@ -0,0 +1,444 @@ +/* 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 = + | 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 ? T : Result>>; + map( + fn: (value: OkType) => T + ): JoinErrorTypes ? T : Result>; + + rollback(): Result | Promise>; +} + +type InferErrorType> = T extends Result ? Errortype : never; + +type InferOkType> = T extends Result ? 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( + value?: OkType, + rollbackFn?: RollbackFn + ): Result { + return new Ok(value || null!, rollbackFn) as any; + } + + export function error( + 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 unknown) | (() => Promise))[]> = Result< + ExtractErrorTypes>, + MapResultTupleToOkTypeTuple>, + HasAsyncRollbackFunction extends true ? () => Promise : () => void + >; + + export function combine 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 + 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 ? T : Result>>; + map( + fn: (value: OkType) => T + ): JoinErrorTypes ? 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 extends Base< + ErrorType, + OkType, + RollbackFn +> { + 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 extends Base< + ErrorType, + OkType, + RollbackFn +> { + 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/helpers/typed_event.ts b/server/src/core/helpers/typed_event.ts new file mode 100644 index 0000000..e7002c3 --- /dev/null +++ b/server/src/core/helpers/typed_event.ts @@ -0,0 +1,42 @@ +export interface Listener { + (event: T): any; +} + +export interface Disposable { + dispose(): void; +} + +export class TypedEvent { + private listeners: Listener[] = []; + public listenersOnces: Listener[] = []; + + on = (listener: Listener): Disposable => { + this.listeners.push(listener); + return { + dispose: () => this.off(listener), + }; + }; + + once = (listener: Listener): void => { + this.listenersOnces.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.listenersOnces.length > 0) { + const toCall = this.listenersOnces; + this.listenersOnces = []; + toCall.forEach((listener) => listener(event)); + } + }; + + pipe = (te: TypedEvent): Disposable => { + return this.on((e) => te.emit(e)); + }; +} diff --git a/server/src/core/helper/worker_computed.ts b/server/src/core/helpers/worker_computed.ts similarity index 71% rename from server/src/core/helper/worker_computed.ts rename to server/src/core/helpers/worker_computed.ts index 5d1a04e..a5ab80a 100644 --- a/server/src/core/helper/worker_computed.ts +++ b/server/src/core/helpers/worker_computed.ts @@ -1,6 +1,6 @@ -import { EXEC_EVENT, EXEC_TYPE, ExecError } from "../model/exec_error_model"; +import { EXEC_EVENT, EXEC_TYPE, ExecError } from "../models/exec_error_model"; import * as cp from "child_process"; -import { ExecutorResult } from "../model/executor_result"; +import { ExecutorResult } from "../models/executor_result"; export enum WorkerType { EXEC = "EXEC", @@ -11,7 +11,7 @@ export interface WorkerDataExec { command: string; execPath: string; type: WorkerType; - cliArgs:Array | undefined + cliArgs: Array | undefined; } process.on("message", async (message) => { @@ -30,15 +30,15 @@ process.on("message", async (message) => { }); } }); - subprocess.on('close', (_code) =>{ + subprocess.on("close", (_code) => { if (process.send) { process.send({ type: EXEC_TYPE.SPAWN.toString(), event: EXEC_EVENT.END.toString(), - data:null + data: null, }); } - }) + }); process.on("uncaughtException", (error) => { if (process.send) { process.send({ @@ -50,26 +50,21 @@ process.on("message", async (message) => { }); } else if (workerData.type == WorkerType.EXEC) { try { - const result = await exec(workerData.command, { + const result = await exec(workerData.command, { cwd: workerData.execPath, }); if (process.send) { - process.send( - new ExecutorResult(EXEC_TYPE.EXEC, EXEC_EVENT.END, result) - ); + process.send(new ExecutorResult(EXEC_TYPE.EXEC, EXEC_EVENT.END, result)); } } catch (error) { - if (process.send) { - process.send(new ExecError(workerData.command, error )); + if (process.send) { + process.send(new ExecError(workerData.command, error)); } } } }); -async function exec( - cmd: string, - opts: cp.ExecOptions & { trim?: boolean } = {} -): Promise { +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/interfaces/file.ts b/server/src/core/interfaces/file.ts new file mode 100644 index 0000000..9fc99c8 --- /dev/null +++ b/server/src/core/interfaces/file.ts @@ -0,0 +1,3 @@ +export interface IFile extends File { + data: Buffer; +} diff --git a/server/src/core/interfaces/payload.ts b/server/src/core/interfaces/payload.ts index f99acbc..d9f7e0a 100644 --- a/server/src/core/interfaces/payload.ts +++ b/server/src/core/interfaces/payload.ts @@ -1,4 +1,3 @@ - // export class Payload{ // model: T | undefined // query:string | undefined @@ -11,4 +10,4 @@ // isEmpty(){ // return this.model != undefined || this.query != undefined // } -// } \ No newline at end of file +// } diff --git a/server/src/core/interfaces/router.ts b/server/src/core/interfaces/router.ts index ad4dbd4..9fabee5 100644 --- a/server/src/core/interfaces/router.ts +++ b/server/src/core/interfaces/router.ts @@ -5,10 +5,7 @@ export interface Routes { } export interface IRouteModel { - validationModel: any; + validationModel?: any; url: string; - databaseModel: any; + databaseModel?: any; } - - - \ No newline at end of file diff --git a/server/src/core/middlewares/validation_auth.ts b/server/src/core/middlewares/validation_auth.ts index d044c30..40f88d5 100644 --- a/server/src/core/middlewares/validation_auth.ts +++ b/server/src/core/middlewares/validation_auth.ts @@ -1,12 +1,13 @@ -// import { RequestHandler } from "express"; +import { RequestHandler } from "express"; -// export const validationMiddleware = ( -// type: any, -// value = 'body', -// skipMissingProperties = false, -// whitelist = true, -// forbidNonWhitelisted = true, -// ): RequestHandler => { - - -// } \ No newline at end of file +export const validationMiddleware = ( + type: any, + value = "body", + skipMissingProperties = false, + whitelist = true, + forbidNonWhitelisted = true +): RequestHandler => { + // TODO:(IDONTSUDO) need TOKEN + // return nextTick + return (req, res, next) => {}; +}; diff --git a/server/src/core/middlewares/validation_model.ts b/server/src/core/middlewares/validation_model.ts index 5a58d72..ec5986d 100644 --- a/server/src/core/middlewares/validation_model.ts +++ b/server/src/core/middlewares/validation_model.ts @@ -1,30 +1,28 @@ -import { plainToInstance } from 'class-transformer'; -import { validate, ValidationError } from 'class-validator'; -import { RequestHandler } from 'express'; - +import { plainToInstance } from "class-transformer"; +import { validate, ValidationError } from "class-validator"; +import { RequestHandler } from "express"; + export const validationModelMiddleware = ( type: any, - value = 'body', + value = "body", skipMissingProperties = false, whitelist = true, - forbidNonWhitelisted = true, + forbidNonWhitelisted = true ): RequestHandler => { return (req, res, next) => { - if(type === null && type == undefined){ - next() - return + if (type === null && type == undefined) { + next(); + return; } const model = plainToInstance(type, req[value]); validate(model, { skipMissingProperties, whitelist, forbidNonWhitelisted }).then((errors: ValidationError[]) => { - console.log(errors) if (errors.length > 0) { - const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(', '); - return res.status(400).json(message) + const message = errors.map((error: ValidationError) => Object.values(error.constraints)).join(", "); + return res.status(400).json(message); } else { - req['model'] = model + req["model"] = model; next(); } }); }; }; - diff --git a/server/src/core/models/active_pipeline_model.ts b/server/src/core/models/active_pipeline_model.ts new file mode 100644 index 0000000..9fa4c4e --- /dev/null +++ b/server/src/core/models/active_pipeline_model.ts @@ -0,0 +1,24 @@ +export class ActivePipeline { + pipelineIsRunning: boolean; + projectUUID?: string | null; + lastProcessCompleteCount: number | null; + error: any; + path: string; + constructor( + pipelineIsRunning: boolean, + lastProcessCompleteCount: number | null, + error: any, + path: string | null, + projectUUID?: string | null + ) { + this.pipelineIsRunning = pipelineIsRunning; + this.projectUUID = projectUUID; + this.lastProcessCompleteCount = lastProcessCompleteCount; + this.error = error; + this.path = path; + } + + static empty() { + return new ActivePipeline(false, null, null, null, null); + } +} diff --git a/server/src/core/model/exec_error_model.ts b/server/src/core/models/exec_error_model.ts similarity index 52% rename from server/src/core/model/exec_error_model.ts rename to server/src/core/models/exec_error_model.ts index d9688cc..bbb9f38 100644 --- a/server/src/core/model/exec_error_model.ts +++ b/server/src/core/models/exec_error_model.ts @@ -1,22 +1,23 @@ +import { extensions } from "../extensions/extensions"; + +extensions(); + export class ExecError extends Error { - static isExecError(e: any) { - if ("type" in e && "script" in e && "unixTime" in e && 'error' in e) { + static isExecError(e: any): ExecError | void { + if ("type" in e && "script" in e && "unixTime" in e && "error" in e) { return new ExecError(e.type, e.event, e.data); } - return; + return; } script: string; unixTime: number; type = EXEC_TYPE.EXEC; - error:any; - constructor( - script: string, - ...args: any - ) { + error: any; + constructor(script: string, ...args: any) { super(...args); this.script = script; this.unixTime = Date.now(); - this.error = args[0] + this.error = args.firstElement(); } } @@ -26,27 +27,15 @@ export class SpawnError extends Error { unixTime: number; type = EXEC_TYPE.SPAWN; - constructor( - environment: string, - script: string, - ...args: any - ) { + 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 - ); + static isError(errorType: any): SpawnError | void { + if ("command" in errorType && "error" in errorType && "execPath" in errorType) { + return new SpawnError(errorType.command, errorType.execPath, errorType.error); } return; } diff --git a/server/src/core/model/executor_result.ts b/server/src/core/models/executor_result.ts similarity index 100% rename from server/src/core/model/executor_result.ts rename to server/src/core/models/executor_result.ts diff --git a/server/src/core/model/meta_data_file_manager_model.ts b/server/src/core/models/meta_data_file_manager_model.ts similarity index 97% rename from server/src/core/model/meta_data_file_manager_model.ts rename to server/src/core/models/meta_data_file_manager_model.ts index 21097c5..f147fab 100644 --- a/server/src/core/model/meta_data_file_manager_model.ts +++ b/server/src/core/models/meta_data_file_manager_model.ts @@ -1,5 +1,4 @@ export enum EventsFileChanger { - remove = "remove", update = "update", delete = "delete", create = "create", @@ -18,6 +17,7 @@ export class MetaDataFileManagerModel { this.event = event; this.unixTime = Date.now(); } + public get timeString(): string { const date = new Date(this.unixTime * 1000); const hours = date.getHours(); diff --git a/server/src/core/model/process_model.ts b/server/src/core/models/process_model.ts similarity index 85% rename from server/src/core/model/process_model.ts rename to server/src/core/models/process_model.ts index e5fa05f..9f601ed 100644 --- a/server/src/core/model/process_model.ts +++ b/server/src/core/models/process_model.ts @@ -1,9 +1,9 @@ -import { Trigger } from "../../features/triggers/trigger_model"; +import { Trigger } from "../../features/triggers/models/trigger_database_model"; import { EXEC_TYPE } from "./exec_error_model"; export interface IPipeline { process: IProcess; - trigger: Trigger; + trigger?: Trigger; env: Env | null; stackGenerateType: StackGenerateType; } diff --git a/server/src/core/repository/fs.ts b/server/src/core/repository/fs.ts new file mode 100644 index 0000000..138681c --- /dev/null +++ b/server/src/core/repository/fs.ts @@ -0,0 +1,31 @@ +import * as fs from "fs"; +import { promisify } from "node:util"; + +export const readFileAsync = promisify(fs.readFile); +export const readdir = promisify(fs.readdir); +export const stat = promisify(fs.stat); +export const lsStat = promisify(fs.lstat); +export const createDir = promisify(fs.mkdir); +export const dirIsExists = promisify(fs.exists); + +export const writeFileAsync = promisify(fs.writeFile); + +export function readDirRecursive(path: string, filesToDir: string[] = []) { + const files = fs.readdirSync(path); + files.forEach((file) => { + let filePath = ""; + if (path[path.length - 1] !== "/") { + filePath = `${path}/${file}`; + } else { + filePath = `${path}${file}`; + } + + const stats = fs.statSync(filePath); + if (stats.isDirectory()) { + readDirRecursive(filePath, filesToDir); + } else { + filesToDir.push(file); + } + }); + return filesToDir; +} diff --git a/server/src/core/repository/http_repository.ts b/server/src/core/repository/http_repository.ts new file mode 100644 index 0000000..2603c4a --- /dev/null +++ b/server/src/core/repository/http_repository.ts @@ -0,0 +1,24 @@ +import axios from "axios"; +import { Result } from "../helpers/result"; +import { HttpMethodType } from "../controllers/http_controller"; + +export class HttpRepository { + serverUrl = "http://localhost:4001"; + + constructor(serverURL: string) { + this.serverUrl = serverURL; + } + + async jsonRequest(url: string, method: HttpMethodType, reqBody?: any): Promise> { + try { + const result = await axios(this.serverUrl + url, { method: method, data: reqBody }); + if (result.status !== 200) { + return Result.error(Error("status code" + String(result.status))); + } + return Result.ok(result.data); + } catch (error) { + console.log(error); + return Result.error(error); + } + } +} diff --git a/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts b/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts new file mode 100644 index 0000000..8fac80e --- /dev/null +++ b/server/src/core/scenarios/set_active_pipeline_to_realtime_service_scenario.ts @@ -0,0 +1,23 @@ +import { + IProjectInstanceModel, + ProjectInstanceDbModel, +} from "../../features/project_instance/models/project_instance_database_model"; +import { pipelineRealTimeService } from "../../features/realtime/realtime_presentation"; +import { App } from "../controllers/app"; +import { CreateFolderUseCase } from "../usecases/crete_folder_usecase"; +import { SearchDataBaseModelUseCase } from "../usecases/search_database_model_usecase"; + +export class SetLastActivePipelineToRealTimeServiceScenario { + call = async (): Promise => { + const result = await new SearchDataBaseModelUseCase(ProjectInstanceDbModel).call({ + isActive: true, + }); + + if (result.isSuccess()) { + const projectModel = result.value; + const projectPath = App.staticFilesStoreDir() + result.value.rootDir + "/"; + await new CreateFolderUseCase().call(projectPath); + pipelineRealTimeService.setPipelineDependency(projectModel.project.pipelines, projectPath, projectModel._id); + } + }; +} diff --git a/server/src/core/services/executor_program_service.ts b/server/src/core/services/executor_program_service.ts index 6107d7d..493046a 100644 --- a/server/src/core/services/executor_program_service.ts +++ b/server/src/core/services/executor_program_service.ts @@ -1,10 +1,10 @@ import cluster, { Worker } from "node:cluster"; -import { TypedEvent } from "../helper/typed_event"; -import { Result } from "../helper/result"; -import { WorkerDataExec, WorkerType } from "../helper/worker_computed"; -import { delay } from "../helper/delay"; -import { ExecutorResult } from "../model/executor_result"; -import { EXEC_TYPE, ExecError, SpawnError } from "../model/exec_error_model"; +import { TypedEvent } from "../helpers/typed_event"; +import { Result } from "../helpers/result"; +import { WorkerDataExec, WorkerType } from "../helpers/worker_computed"; +import { delay } from "../helpers/delay"; +import { ExecutorResult } from "../models/executor_result"; +import { EXEC_TYPE, ExecError, SpawnError } from "../models/exec_error_model"; abstract class IExecutorProgramService { abstract execPath: string; @@ -25,13 +25,9 @@ export class ExecutorProgramService this.maxTime = maxTime; } - private async workerExecuted( - command: string, - workerType: WorkerType, - args: Array | undefined = undefined - ) { + private async workerExecuted(command: string, workerType: WorkerType, args: Array | undefined = undefined) { cluster.setupPrimary({ - exec: "./src/core/helper/worker_computed", + exec: "/Users/idontsudo/Desktop/testdeck-mocha-seed/server/build/src/core/helpers/worker_computed.js", }); const worker = cluster.fork(); @@ -69,21 +65,13 @@ export class ExecutorProgramService setTimeout(() => { this.worker.kill(); this.emit( - Result.error( - WorkerType.EXEC - ? new ExecError(command, "timeout err") - : new SpawnError(command, "timeout err") - ) + 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 { + public async call(type: EXEC_TYPE, command: string, args: Array | undefined = undefined): Promise { if (type == EXEC_TYPE.EXEC) { this.workerExecuted(command, WorkerType.EXEC); diff --git a/server/src/core/services/files_change_notifier_service.ts b/server/src/core/services/files_change_notifier_service.ts index af0d397..76bea96 100644 --- a/server/src/core/services/files_change_notifier_service.ts +++ b/server/src/core/services/files_change_notifier_service.ts @@ -1,20 +1,11 @@ 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"; -import { Result } from "../helper/result"; -import { TypedEvent } from "../helper/typed_event"; - -const readFileAsync = promisify(fs.readFile); -const readdir = promisify(fs.readdir); -const stat = promisify(fs.stat); -const lsStat = promisify(fs.lstat); +import { EventsFileChanger, MetaDataFileManagerModel } from "../models/meta_data_file_manager_model"; +import { Result } from "../helpers/result"; +import { TypedEvent } from "../helpers/typed_event"; +import { lsStat, readFileAsync, readdir, stat } from "../repository/fs"; function joinBuffers(buffers: Array, delimiter = " ") { const d = Buffer.from(delimiter); @@ -27,13 +18,11 @@ async function readFileAtBuffer(path: string): Promise { await readdir(path, { encoding: "buffer", }) - ).reduce( - (accumulator, currentValue) => joinBuffers([accumulator, currentValue]), - Buffer.from("") - ); + ).reduce((accumulator, currentValue) => joinBuffers([accumulator, currentValue]), Buffer.from("")); } return await readFileAsync(path); } + function md5(content: Buffer | BinaryLike): Promise { return new Promise((resolve, _reject) => { return resolve(createHash("md5").update(content).digest("hex")); @@ -47,6 +36,7 @@ export interface IHashesCache { export abstract class IFilesChangeNotifierService { abstract directory: string; } + export class FilesChangeNotifierService extends TypedEvent> implements IFilesChangeNotifierService @@ -69,11 +59,7 @@ export class FilesChangeNotifierService const data = await readFileAsync(file); const md5Current = await md5(data); - this.hashes[file] = new MetaDataFileManagerModel( - file, - md5Current, - EventsFileChanger.static - ); + this.hashes[file] = new MetaDataFileManagerModel(file, md5Current, EventsFileChanger.static); this.emit(Result.ok(this.hashes)); } async getFiles(dir: string): Promise { @@ -91,46 +77,31 @@ export class FilesChangeNotifierService 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 readFileAtBuffer(filePath); - const md5Current = await 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 - ); + 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 readFileAtBuffer(filePath); + const md5Current = await 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) { diff --git a/server/src/core/services/pipeline_real_time_service.ts b/server/src/core/services/pipeline_real_time_service.ts new file mode 100644 index 0000000..479b7b2 --- /dev/null +++ b/server/src/core/services/pipeline_real_time_service.ts @@ -0,0 +1,73 @@ +import { TypedEvent } from "../helpers/typed_event"; +import { ExecError } from "../models/exec_error_model"; +import { ExecutorResult } from "../models/executor_result"; +import { ActivePipeline } from "../models/active_pipeline_model"; +import { IPipeline } from "../models/process_model"; +import { Iteration } from "./stack_service"; + +export class PipelineRealTimeService extends TypedEvent { + status: ActivePipeline; + pipelineModels?: IPipeline[]; + constructor() { + super(); + this.init(); + } + private init(): void { + this.status = ActivePipeline.empty(); + } + pipelineSubscriber = (iterations: Iteration[]): void => { + if (this.status["lastProcessCompleteCount"] === 0) { + this.status["lastProcessCompleteCount"] = 0; + } + this.status.lastProcessCompleteCount += 1; + this.pipelineCompleted(); + this.iterationHelper(iterations[iterations.length - 1]); + this.emit(this.status); + }; + + iterationHelper(iteration: Iteration): void { + this.iterationErrorObserver(iteration); + + // TODO(IDONTSUDO): implements + this.iterationLogSaver(iteration); + } + + iterationLogSaver(iteration: Iteration): void { + // throw new Error("Method not implemented."); + // citeration.result.data + } + + iterationErrorObserver(iteration: Iteration): void { + if (this.status.lastProcessCompleteCount === 1) { + return; + } + const result = iteration?.result; + const executorResult = ExecutorResult.isExecutorResult(result); + const execError = ExecError.isExecError(result); + + if (executorResult instanceof ExecutorResult) { + this.status.error = executorResult; + } + + if (execError instanceof ExecError) { + this.status.error = execError; + } + } + pipelineCompleted() { + const pipelineIsCompleted = this.status.lastProcessCompleteCount === this.pipelineModels?.length; + if (pipelineIsCompleted) { + this.status.pipelineIsRunning = false; + } + } + setPipelineDependency(pipelineModels: IPipeline[], path: string, projectUUID: string) { + this.pipelineModels = pipelineModels; + this.status["projectUUID"] = projectUUID; + this.status["path"] = path; + } + runPipeline(): void { + // const stack = new StackService(this.pipelineModels, this.path); + // this.status["pipelineIsRunning"] = true; + // stack.on(this.pipelineSubscriber); + // stack.call(); + } +} diff --git a/server/src/core/services/stack_service.ts b/server/src/core/services/stack_service.ts index ceb6fbf..32d686e 100644 --- a/server/src/core/services/stack_service.ts +++ b/server/src/core/services/stack_service.ts @@ -1,16 +1,13 @@ -import { - FilesChangeNotifierService, - IHashesCache, -} from "./files_change_notifier_service"; -import { IPipeline } from "../model/process_model"; +import { FilesChangeNotifierService, IHashesCache } from "./files_change_notifier_service"; +import { IPipeline } from "../models/process_model"; import { ExecutorProgramService } from "./executor_program_service"; -import { EXEC_EVENT, ExecError, SpawnError } from "../model/exec_error_model"; -import { TypedEvent } from "../helper/typed_event"; -import { Result } from "../helper/result"; -import { ExecutorResult } from "../model/executor_result"; -import { delay } from "../helper/delay"; +import { EXEC_EVENT, ExecError, SpawnError } from "../models/exec_error_model"; +import { TypedEvent } from "../helpers/typed_event"; +import { Result } from "../helpers/result"; +import { ExecutorResult } from "../models/executor_result"; +import { delay } from "../helpers/delay"; import { TriggerService } from "./trigger_service"; -import { Trigger } from "../../features/triggers/trigger_model"; +import { Trigger } from "../../features/triggers/models/trigger_database_model"; export interface Iteration { hashes: IHashesCache | null; @@ -24,7 +21,7 @@ export abstract class IStackService { abstract init(processed: IPipeline[], path: string): void; } -export class StackService extends TypedEvent implements IStackService { +export class StackService extends TypedEvent implements IStackService { callStack: Iteration[]; path: string; constructor(processed: IPipeline[], path: string) { @@ -44,10 +41,7 @@ export class StackService extends TypedEvent implements IStackService { } } private commandHandler(processMetaData: IPipeline) { - processMetaData.process.command = processMetaData.process.command.replace( - "$PATH", - this.path - ); + processMetaData.process.command = processMetaData.process.command.replace("$PATH", this.path); return processMetaData; } public async call() { @@ -56,36 +50,23 @@ export class StackService extends TypedEvent implements IStackService { for await (const el of this.callStack!) { await this.execStack(inc, el); inc += 1; + this.emit(this.callStack); } } - async execStack( - stackNumber: number, - stackLayer: Iteration - ): Promise { + async execStack(stackNumber: number, stackLayer: Iteration): Promise { const executorService = new ExecutorProgramService(this.path); + executorService.call(stackLayer.process.process.type, stackLayer.process.process.command); - executorService.call( - stackLayer.process.process.type, - stackLayer.process.process.command - ); - - const filesChangeNotifierService = new FilesChangeNotifierService( - this.path - ); + const filesChangeNotifierService = new FilesChangeNotifierService(this.path); filesChangeNotifierService.call(); - const result = await this.waitEvent< - Result - >(executorService); + const result = await this.waitEvent>(executorService); await delay(100); if (result.isSuccess()) { this.callStack[stackNumber].result = result.value; this.callStack[stackNumber].hashes = filesChangeNotifierService.hashes; - const triggerResult = await this.triggerExec( - stackLayer.process.trigger, - stackNumber - ); + const triggerResult = await this.triggerExec(stackLayer.process.trigger, stackNumber); triggerResult.fold( (s) => { s; @@ -120,15 +101,15 @@ export class StackService extends TypedEvent implements IStackService { }); return promise; } - private async triggerExec( - trigger: Trigger, - stackNumber: number - ): Promise> { - const hashes = this.callStack[stackNumber].hashes; + private async triggerExec(trigger: Trigger | null, stackNumber: number): Promise> { + if (trigger !== null) { + const hashes = this.callStack[stackNumber].hashes; - if (hashes != null) { - return await new TriggerService(trigger, hashes, this.path).call(); + if (hashes != null) { + return await new TriggerService(trigger, hashes, this.path).call(); + } + throw new Error("Hashes is null"); } - throw new Error("Hashes is null"); + return Result.ok(); } } diff --git a/server/src/core/services/trigger_service.ts b/server/src/core/services/trigger_service.ts index 0c7b990..e4979f1 100644 --- a/server/src/core/services/trigger_service.ts +++ b/server/src/core/services/trigger_service.ts @@ -1,9 +1,9 @@ import * as vm from "node:vm"; import { IHashesCache } from "./files_change_notifier_service"; -import { EventsFileChanger } from "../model/meta_data_file_manager_model"; -import { Result } from "../helper/result"; -import { TypedEvent } from "../helper/typed_event"; -import { Trigger, TriggerType } from "../../features/triggers/trigger_model"; +import { EventsFileChanger } from "../models/meta_data_file_manager_model"; +import { Result } from "../helpers/result"; +import { TypedEvent } from "../helpers/typed_event"; +import { Trigger, TriggerType } from "../../features/triggers/models/trigger_database_model"; export class TriggerCallResult { results: Array; @@ -35,11 +35,7 @@ export class TriggerErrorReport extends Error { hashes: IHashesCache; trigger: string | Trigger; processOutput: any; - constructor( - hashes: IHashesCache, - trigger: string | Trigger, - processOutput?: any - ) { + constructor(hashes: IHashesCache, trigger: string | Trigger, processOutput?: any) { super(); this.hashes = hashes; this.trigger = trigger; @@ -123,9 +119,7 @@ export class TriggerService extends TypedEvent { }) ); } - private reportTriggerTypeProcess( - triggerResult: Array - ): TriggerCallResult { + private reportTriggerTypeProcess(triggerResult: Array): TriggerCallResult { return new TriggerCallResult( triggerResult.map((el) => { if (el.status) { diff --git a/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts b/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts new file mode 100644 index 0000000..5780d8b --- /dev/null +++ b/server/src/core/usecases/check_and_create_static_files_folder_usecase.ts @@ -0,0 +1,19 @@ +import { App } from "../controllers/app"; +import { dirIsExists } from "../repository/fs"; +import { CreateFolderUseCase } from "./crete_folder_usecase"; + +export class CheckAndCreateStaticFilesFolderUseCase { + call = async (): Promise => { + if (await dirIsExists(App.staticFilesStoreDir())) { + return; + } + const createFolderUseCase = await new CreateFolderUseCase().call(App.staticFilesStoreDir()); + + createFolderUseCase.fold( + (_s) => {}, + (e) => { + console.log(e); + } + ); + }; +} diff --git a/server/src/core/usecases/create_database_model_usecase.ts b/server/src/core/usecases/create_database_model_usecase.ts index a73d071..4a04b59 100644 --- a/server/src/core/usecases/create_database_model_usecase.ts +++ b/server/src/core/usecases/create_database_model_usecase.ts @@ -1,4 +1,4 @@ -import { Result } from "../helper/result"; +import { Result } from "../helpers/result"; import { ICreateObjectDataBase } from "../interfaces/response"; export class CreateDataBaseModelUseCase { @@ -8,9 +8,7 @@ export class CreateDataBaseModelUseCase { this.databaseModel = model; } - call = async ( - validationModel: V - ): Promise> => { + call = async (validationModel: V): Promise> => { try { const result = new this.databaseModel(validationModel); diff --git a/server/src/core/usecases/create_file_usecase.ts b/server/src/core/usecases/create_file_usecase.ts new file mode 100644 index 0000000..5795336 --- /dev/null +++ b/server/src/core/usecases/create_file_usecase.ts @@ -0,0 +1,13 @@ +import { Result } from "../helpers/result"; +import { writeFileAsync } from "../repository/fs"; + +export class CreateFileUseCase { + async call(path: string, buffer: Buffer): Promise> { + try { + await writeFileAsync(path, buffer); + return Result.ok(); + } catch (err) { + return Result.error(err as Error); + } + } +} diff --git a/server/src/core/usecases/crete_folder_usecase.ts b/server/src/core/usecases/crete_folder_usecase.ts new file mode 100644 index 0000000..58da331 --- /dev/null +++ b/server/src/core/usecases/crete_folder_usecase.ts @@ -0,0 +1,17 @@ +import { Result } from "../helpers/result"; +import { dirIsExists, createDir } from "../repository/fs"; + +export class CreateFolderUseCase { + call = async (path: string): Promise> => { + try { + if (await dirIsExists(path)) { + return Result.error(new Error("createFolderUseCase create dir ")); + } + await createDir(path); + + return Result.ok("ok"); + } catch (error) { + return Result.error(error as Error); + } + }; +} diff --git a/server/src/core/usecases/database_connect_usecase.ts b/server/src/core/usecases/database_connect_usecase.ts new file mode 100644 index 0000000..9b4d3b8 --- /dev/null +++ b/server/src/core/usecases/database_connect_usecase.ts @@ -0,0 +1,15 @@ +import mongoose from "mongoose"; +import { Result } from "../helpers/result"; + +export class DataBaseConnectUseCase { + call = async (dataBaseName: string = "test"): Promise> => { + try { + await mongoose.connect(`mongodb://127.0.0.1:27017/${dataBaseName}`); + return Result.ok(); + } catch (error) { + console.log(error); + console.log("database connect error"); + return Result.error(error as Error); + } + }; +} diff --git a/server/src/core/usecases/delete_database_model_usecase.ts b/server/src/core/usecases/delete_database_model_usecase.ts index abc97a6..07dcc51 100644 --- a/server/src/core/usecases/delete_database_model_usecase.ts +++ b/server/src/core/usecases/delete_database_model_usecase.ts @@ -1,16 +1,21 @@ - import { Result } from "../helper/result"; +import { Result } from "../helpers/result"; export class DeleteDataBaseModelUseCase { - databaseModel: D | any; + databaseModel: D | any; constructor(model) { this.databaseModel = model; } - call = async (id: string): Promise> => { + call = async (id: string): Promise> => { try { - const model = new this.databaseModel({ _id: id }); - await model.deleteOne(); + const model = this.databaseModel as any; - return Result.ok(true); + const result = await model.deleteOne({ _id: id }); + + if (result.deletedCount === 0) { + return Result.error(Error(`the database does not have a collection with this ID:${id}`)); + } + + return Result.ok({ ok: "model delete" }); } catch (error) { return Result.error(error); } diff --git a/server/src/core/usecases/drop_database_usecase.ts b/server/src/core/usecases/drop_database_usecase.ts new file mode 100644 index 0000000..a9a4d6b --- /dev/null +++ b/server/src/core/usecases/drop_database_usecase.ts @@ -0,0 +1,7 @@ +import mongoose from "mongoose"; + +export class DropDataBaseUseCase { + call = async () => { + await mongoose.connection.dropDatabase(); + }; +} diff --git a/server/src/core/usecases/exit_app_usecase.ts b/server/src/core/usecases/exit_app_usecase.ts new file mode 100644 index 0000000..2a7498a --- /dev/null +++ b/server/src/core/usecases/exit_app_usecase.ts @@ -0,0 +1,5 @@ +export class ExitAppUseCase { + call = () => { + process.exit(); + }; +} diff --git a/server/src/core/usecases/pagination_database_model_usecase.ts b/server/src/core/usecases/pagination_database_model_usecase.ts index ada4d6a..e1a95cf 100644 --- a/server/src/core/usecases/pagination_database_model_usecase.ts +++ b/server/src/core/usecases/pagination_database_model_usecase.ts @@ -1,4 +1,4 @@ -import { Result } from "../helper/result"; +import { Result } from "../helpers/result"; export class PaginationDataBaseModelUseCase { databaseModel: D; @@ -9,9 +9,7 @@ export class PaginationDataBaseModelUseCase { this.perPage = perPage; } - call = async ( - pageNumber: number - ): Promise> => { + call = async (pageNumber: number): Promise> => { try { const page = Math.max(0, pageNumber); const model = this.databaseModel as any; diff --git a/server/src/core/usecases/read_by_id_database_model_usecase.ts b/server/src/core/usecases/read_by_id_database_model_usecase.ts new file mode 100644 index 0000000..8d14c0f --- /dev/null +++ b/server/src/core/usecases/read_by_id_database_model_usecase.ts @@ -0,0 +1,17 @@ +import { Result } from "../helpers/result"; + +export class ReadByIdDataBaseModelUseCase { + databaseModel: D; + + constructor(model) { + this.databaseModel = model; + } + call = async (id: string): Promise> => { + try { + const dbModel = this.databaseModel as any; + return Result.ok(await dbModel.findById(id)); + } catch (error) { + return Result.error(error); + } + }; +} diff --git a/server/src/core/usecases/read_database_model_usecase.ts b/server/src/core/usecases/read_database_model_usecase.ts index 6786b80..1de0ab6 100644 --- a/server/src/core/usecases/read_database_model_usecase.ts +++ b/server/src/core/usecases/read_database_model_usecase.ts @@ -1,5 +1,4 @@ - -import { Result } from "../helper/result"; +import { Result } from "../helpers/result"; export class ReadByIdDataBaseModelUseCase { databaseModel: D; diff --git a/server/src/core/usecases/search_database_model_usecase.ts b/server/src/core/usecases/search_database_model_usecase.ts new file mode 100644 index 0000000..44ad0d2 --- /dev/null +++ b/server/src/core/usecases/search_database_model_usecase.ts @@ -0,0 +1,18 @@ +import { Result } from "../helpers/result"; + +export class SearchDataBaseModelUseCase { + model: any; + + constructor(model: any) { + this.model = model; + } + + call = async (findFilter: Partial): Promise> => { + const result = await this.model.findOne(findFilter); + if (result === null) { + return Result.error(null); + } else { + return Result.ok(result); + } + }; +} diff --git a/server/src/core/usecases/update_database_model_usecase.ts b/server/src/core/usecases/update_database_model_usecase.ts index a35b788..c828d19 100644 --- a/server/src/core/usecases/update_database_model_usecase.ts +++ b/server/src/core/usecases/update_database_model_usecase.ts @@ -1,4 +1,4 @@ -import { Result } from "../helper/result"; +import { Result } from "../helpers/result"; interface uuid { _id?: string; @@ -17,7 +17,14 @@ export class UpdateDataBaseModelUseCase { } const databaseModel = this.databaseModel as any; const model = await databaseModel.findById(updateModel._id); + if (model === null) { + throw new Error( + `we can’t find the model in the database with ID:${updateModel._id} collection: ${databaseModel.modelName}` + ); + } + Object.assign(model, updateModel); + await model.save(); return Result.ok(model as T); } catch (error) { diff --git a/server/src/features/nix_store_manager/nix_store_manager.ts b/server/src/features/nix_store_manager/nix_store_manager.ts new file mode 100644 index 0000000..145bfd5 --- /dev/null +++ b/server/src/features/nix_store_manager/nix_store_manager.ts @@ -0,0 +1,58 @@ +import { CallbackStrategyWithEmpty, CoreHttpController } from "../../core/controllers/http_controller"; +import { Result } from "../../core/helpers/result"; +import { EXEC_TYPE } from "../../core/models/exec_error_model"; +import { ExecutorResult } from "../../core/models/executor_result"; +import { IPipeline, IssueType, StackGenerateType } from "../../core/models/process_model"; +import { StackService } from "../../core/services/stack_service"; +import { TriggerType } from "../triggers/models/trigger_database_model"; + +class NixStoreModel {} + +const getNixStoreFolderCommand: IPipeline[] = [ + { + process: { + type: EXEC_TYPE.EXEC, + command: `ls /nix/store`, + isGenerating: true, + isLocaleCode: false, + issueType: IssueType.WARNING, + }, + trigger: { + type: TriggerType.FILE, + value: ["context"], + }, + env: null, + stackGenerateType: StackGenerateType.SINGLETON, + }, +]; +class GetNixStorePackagesUseCase extends CallbackStrategyWithEmpty { + call = async () => { + const stackService = new StackService( + getNixStoreFolderCommand, + "/Users/idontsudo/Desktop/testdeck-mocha-seed/server/build/test/" + ); + stackService.call(); + + const promise = new Promise((resolve, _reject) => { + stackService.on((e) => { + const iteration = e.firstElement(); + if (iteration.result instanceof ExecutorResult) { + const nixPackage = iteration.result.data; + resolve(nixPackage.split("\n").filter((e) => e.hasNoPattern(".drv"))); + } else { + return "GetNixStorePackagesUseCase unknown Error"; + } + }); + }); + return Result.ok(await promise); + }; +} + +export class NixStoreManagerPresentation extends CoreHttpController { + constructor() { + super({ + url: "nix_store_api", + }); + super.get(new GetNixStorePackagesUseCase().call); + } +} diff --git a/server/src/features/pipelines/models/pipeline_database_model.ts b/server/src/features/pipelines/models/pipeline_database_model.ts new file mode 100644 index 0000000..1d39bd7 --- /dev/null +++ b/server/src/features/pipelines/models/pipeline_database_model.ts @@ -0,0 +1,27 @@ +import { Schema, model } from "mongoose"; +import { IPipeline } from "../../../core/models/process_model"; +import { schemaProcess } from "../../process/models/process_database_model"; +import { triggerSchema } from "../../triggers/models/trigger_database_model"; + +export const PipelineSchema = new Schema({ + process: { + type: Schema.Types.ObjectId, + ref: schemaProcess, + autopopulate: true, + default: null, + }, + trigger: { + type: Schema.Types.ObjectId, + ref: triggerSchema, + autopopulate: true, + default: null, + }, + stackGenerateType: { + type: String, + default: null, + }, +}).plugin(require("mongoose-autopopulate")); + +export const schemaPipeline = "Pipeline"; + +export const PipelineDBModel = model(schemaPipeline, PipelineSchema); diff --git a/server/src/features/pipelines/models/pipeline_model.ts b/server/src/features/pipelines/models/pipeline_model.ts new file mode 100644 index 0000000..69cd0fc --- /dev/null +++ b/server/src/features/pipelines/models/pipeline_model.ts @@ -0,0 +1,21 @@ +import { IsOptional, ValidateNested } from "class-validator"; +import { IPipeline, IProcess, StackGenerateType } from "../../../core/models/process_model"; +import { Type } from "class-transformer"; +import { ProcessModel } from "../../process/models/process_validation_model"; +import { TriggerModelValidationModel } from "../../triggers/models/trigger_validation_model"; + +export class PipelineModel implements IPipeline { + @ValidateNested() + @Type(() => ProcessModel) + public process: IProcess; + + @ValidateNested() + @Type(() => TriggerModelValidationModel) + public trigger: TriggerModelValidationModel; + + @IsOptional() + public env = null; + + @IsOptional() + public stackGenerateType: StackGenerateType; +} diff --git a/server/src/features/pipelines/models/pipeline_validation_model.ts b/server/src/features/pipelines/models/pipeline_validation_model.ts new file mode 100644 index 0000000..d16382e --- /dev/null +++ b/server/src/features/pipelines/models/pipeline_validation_model.ts @@ -0,0 +1,17 @@ +import { IsMongoId, IsOptional } from "class-validator"; +import { IProcess, StackGenerateType } from "../../../core/models/process_model"; +import { TriggerModelValidationModel } from "../../triggers/models/trigger_validation_model"; + +export class PipelineValidationModel { + @IsMongoId() + public process: IProcess; + + @IsMongoId() + public trigger: TriggerModelValidationModel; + + @IsOptional() + public env = null; + + @IsOptional() + public stackGenerateType: StackGenerateType; +} diff --git a/server/src/features/pipelines/pipeline_model.ts b/server/src/features/pipelines/pipeline_model.ts index 3409dd0..6491ed3 100644 --- a/server/src/features/pipelines/pipeline_model.ts +++ b/server/src/features/pipelines/pipeline_model.ts @@ -1,11 +1,8 @@ import { IsMongoId, IsEnum } from "class-validator"; import { Schema, model } from "mongoose"; -import { StackGenerateType } from "../../core/model/process_model"; -import { - TriggerModel, - triggerSchema, -} from "../triggers/trigger_model"; +import { TriggerModel, triggerSchema } from "../triggers/trigger_model"; import { schemaProcess } from "../process/process_model"; +import { StackGenerateType } from "../../core/models/process_model"; export const PipelineSchema = new Schema({ process: { @@ -27,10 +24,7 @@ export const PipelineSchema = new Schema({ export const schemaPipeline = "Pipeline"; -export const PipelineDBModel = model( - schemaPipeline, - PipelineSchema -); +export const PipelineDBModel = model(schemaPipeline, PipelineSchema); export class PipelineModel { @IsMongoId() diff --git a/server/src/features/pipelines/pipeline_presentation.ts b/server/src/features/pipelines/pipeline_presentation.ts index 6476392..eefbe57 100644 --- a/server/src/features/pipelines/pipeline_presentation.ts +++ b/server/src/features/pipelines/pipeline_presentation.ts @@ -1,16 +1,13 @@ import { CrudController } from "../../core/controllers/crud_controller"; -import { PipelineDBModel, PipelineModel } from "./pipeline_model"; +import { PipelineDBModel } from "./models/pipeline_database_model"; +import { PipelineValidationModel } from "./models/pipeline_validation_model"; -export class PipelinePresentation extends CrudController< - PipelineModel, - typeof PipelineDBModel -> { +export class PipelinePresentation extends CrudController { constructor() { super({ url: "pipeline", - validationModel: PipelineModel, + validationModel: PipelineValidationModel, databaseModel: PipelineDBModel, }); } - } diff --git a/server/src/features/process/models/process_database_model.ts b/server/src/features/process/models/process_database_model.ts new file mode 100644 index 0000000..1617cb0 --- /dev/null +++ b/server/src/features/process/models/process_database_model.ts @@ -0,0 +1,35 @@ +import { Schema, model } from "mongoose"; +import { IProcess } from "../../../core/models/process_model"; + +export const ProcessSchema = new Schema({ + type: { + type: String, + }, + description: { + type: String, + }, + command: { + type: String, + }, + isGenerating: { + type: String, + }, + isLocaleCode: { + type: String, + }, + issueType: { + type: String, + }, + timeout: { + type: Number, + default: null, + }, + commit: { + type: String, + default: null, + }, +}); + +export const schemaProcess = "Process"; + +export const ProcessDBModel = model(schemaProcess, ProcessSchema); diff --git a/server/src/features/process/models/process_validation_model.ts b/server/src/features/process/models/process_validation_model.ts new file mode 100644 index 0000000..7dde139 --- /dev/null +++ b/server/src/features/process/models/process_validation_model.ts @@ -0,0 +1,31 @@ +import { IsBoolean, IsEnum, IsNumber, IsOptional, IsString } from "class-validator"; +import { EXEC_TYPE } from "../../../core/models/exec_error_model"; +import { IProcess, IssueType } from "../../../core/models/process_model"; + +export class ProcessModel implements IProcess { + @IsEnum(EXEC_TYPE) + public type: EXEC_TYPE; + + @IsString() + public description: string; + + @IsString() + public command: string; + + @IsBoolean() + public isGenerating: boolean; + + @IsBoolean() + public isLocaleCode: boolean; + + @IsEnum(IssueType) + public issueType: IssueType; + + @IsOptional() + @IsNumber() + public timeout?: number; + + @IsOptional() + @IsString() + public commit?: string; +} diff --git a/server/src/features/process/process_model.ts b/server/src/features/process/process_model.ts index df48590..a8105c1 100644 --- a/server/src/features/process/process_model.ts +++ b/server/src/features/process/process_model.ts @@ -1,16 +1,7 @@ -import { - IsString, - IsOptional, - IsEnum, - IsNumber, - IsBoolean, -} from "class-validator"; +import { IsString, IsOptional, IsEnum, IsNumber, IsBoolean } from "class-validator"; import { Schema, model } from "mongoose"; -import { - IProcess, - IssueType, -} from "../../core/model/process_model"; -import { EXEC_TYPE } from "../../core/model/exec_error_model"; +import { IProcess, IssueType } from "../../core/models/process_model"; +import { EXEC_TYPE } from "../../core/models/exec_error_model"; export const ProcessSchema = new Schema({ type: { @@ -61,9 +52,8 @@ export class ProcessModel implements IProcess { @IsOptional() @IsNumber() public timeout?: number; - + @IsOptional() @IsString() public commit?: string; } - \ No newline at end of file diff --git a/server/src/features/process/process_presentation.ts b/server/src/features/process/process_presentation.ts index b9ec912..0967f68 100644 --- a/server/src/features/process/process_presentation.ts +++ b/server/src/features/process/process_presentation.ts @@ -1,10 +1,8 @@ import { CrudController } from "../../core/controllers/crud_controller"; -import { ProcessDBModel, ProcessModel } from "./process_model"; +import { ProcessDBModel } from "./models/process_database_model"; +import { ProcessModel } from "./models/process_validation_model"; -export class ProcessPresentation extends CrudController< - ProcessModel, - typeof ProcessDBModel -> { +export class ProcessPresentation extends CrudController { constructor() { super({ url: "process", diff --git a/server/src/features/project_instance/domain/create_new_project_scenario.ts b/server/src/features/project_instance/domain/create_new_project_scenario.ts new file mode 100644 index 0000000..b5a162a --- /dev/null +++ b/server/src/features/project_instance/domain/create_new_project_scenario.ts @@ -0,0 +1,30 @@ +import { App } from "../../../core/controllers/app"; +import { Result } from "../../../core/helpers/result"; +import { CreateDataBaseModelUseCase } from "../../../core/usecases/create_database_model_usecase"; +import { CreateFolderUseCase } from "../../../core/usecases/crete_folder_usecase"; +import { ProjectInstanceDbModel } from "../models/project_instance_database_model"; +import { ProjectInstanceValidationModel } from "../models/project_instance_validation_model"; +import { v4 as uuidv4 } from "uuid"; + +export class CreateNewProjectInstanceScenario { + call = async (model: ProjectInstanceValidationModel): Promise> => { + try { + const folderName = uuidv4() + "/"; + const createFolderUseCase = await new CreateFolderUseCase().call(App.staticFilesStoreDir() + folderName); + if (createFolderUseCase.isFailure()) { + return createFolderUseCase.forward(); + } + model.rootDir = folderName; + + const createDataBaseModelUseCase = await new CreateDataBaseModelUseCase(ProjectInstanceDbModel).call(model); + + if (createDataBaseModelUseCase.isFailure()) { + return createDataBaseModelUseCase.forward(); + } + + return Result.ok({ status: "ok" }); + } catch (error) { + return Result.error(error as Error); + } + }; +} diff --git a/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts b/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts new file mode 100644 index 0000000..e2ab3e5 --- /dev/null +++ b/server/src/features/project_instance/domain/upload_file_to_to_project_scenario.ts @@ -0,0 +1,23 @@ +import { CallbackStrategyWithFileUpload, ResponseBase } from "../../../core/controllers/http_controller"; +import { Result } from "../../../core/helpers/result"; +import { IFile } from "../../../core/interfaces/file"; +import { CreateFileUseCase } from "../../../core/usecases/create_file_usecase"; +import { PipelineStatusUseCase } from "../../realtime/domain/pipeline_status_usecase"; + +export class UploadCadFileToProjectScenario extends CallbackStrategyWithFileUpload { + checkingFileExpression: RegExp = RegExp(".FCStd"); + + async call(file: IFile): ResponseBase { + const pipelineStatusUseCase = await new PipelineStatusUseCase().call(); + if (pipelineStatusUseCase.isFailure()) { + return pipelineStatusUseCase.forward(); + } + const projectFolder = pipelineStatusUseCase.value.path; + const createFileUseCase = await new CreateFileUseCase().call(projectFolder + file.name, file.data); + if (createFileUseCase.isFailure()) { + return createFileUseCase.forward(); + } + + return Result.ok("ok"); + } +} diff --git a/server/src/features/project_instance/models/project_instance_database_model.ts b/server/src/features/project_instance/models/project_instance_database_model.ts new file mode 100644 index 0000000..b0111bc --- /dev/null +++ b/server/src/features/project_instance/models/project_instance_database_model.ts @@ -0,0 +1,33 @@ +import { Schema, model } from "mongoose"; +import { IProjectModel, projectSchema } from "../../projects/models/project_database_model"; + +export interface IProjectInstanceModel { + _id: string; + project: IProjectModel; + description: string; + rootDir: string; + isActive: boolean; +} + +export const ProjectInstanceSchema = new Schema({ + project: { + type: Schema.Types.ObjectId, + ref: projectSchema, + autopopulate: true, + default: null, + }, + description: { + type: String, + }, + rootDir: { + type: String, + }, + isActive: { + type: Boolean, + default: false, + }, +}).plugin(require("mongoose-autopopulate")); + +export const schemaProjectInstance = "instance_project"; + +export const ProjectInstanceDbModel = model(schemaProjectInstance, ProjectInstanceSchema); diff --git a/server/src/features/project_instance/models/project_instance_validation_model.ts b/server/src/features/project_instance/models/project_instance_validation_model.ts new file mode 100644 index 0000000..709cc94 --- /dev/null +++ b/server/src/features/project_instance/models/project_instance_validation_model.ts @@ -0,0 +1,12 @@ +import { IsMongoId, IsOptional, IsString } from "class-validator"; + +export class ProjectInstanceValidationModel { + @IsMongoId() + public project: string; + + @IsString() + public description: string; + + @IsOptional() + public rootDir: string; +} diff --git a/server/src/features/project_instance/project_instance_presentation.ts b/server/src/features/project_instance/project_instance_presentation.ts new file mode 100644 index 0000000..77040a8 --- /dev/null +++ b/server/src/features/project_instance/project_instance_presentation.ts @@ -0,0 +1,27 @@ +import { CrudController } from "../../core/controllers/crud_controller"; +import { CreateNewProjectInstanceScenario } from "./domain/create_new_project_scenario"; +import { UploadCadFileToProjectScenario } from "./domain/upload_file_to_to_project_scenario"; +import { ProjectInstanceDbModel } from "./models/project_instance_database_model"; +import { ProjectInstanceValidationModel } from "./models/project_instance_validation_model"; + +export class ProjectInstancePresentation extends CrudController< + ProjectInstanceValidationModel, + typeof ProjectInstanceDbModel +> { + constructor() { + super({ + validationModel: ProjectInstanceValidationModel, + url: "project_instance", + databaseModel: ProjectInstanceDbModel, + }); + super.post(new CreateNewProjectInstanceScenario().call); + + super.subRoutes = [ + { + method: "POST", + subUrl: "upload", + fn: new UploadCadFileToProjectScenario(), + }, + ]; + } +} diff --git a/server/src/features/projects/models/project_database_model.ts b/server/src/features/projects/models/project_database_model.ts new file mode 100644 index 0000000..2948f94 --- /dev/null +++ b/server/src/features/projects/models/project_database_model.ts @@ -0,0 +1,31 @@ +import { Schema, model } from "mongoose"; +import { schemaPipeline } from "../../pipelines/models/pipeline_database_model"; +import { PipelineValidationModel } from "../../pipelines/models/pipeline_validation_model"; + +export interface IProjectModel { + _id?: string; + pipelines: [PipelineValidationModel]; + rootDir: string; + description: string; + isActive: boolean; +} + +export const ProjectSchema = new Schema({ + pipelines: { + type: Array, + ref: schemaPipeline, + autopopulate: true, + default: null, + }, + description: { + type: String, + }, + isActive: { + type: Boolean, + default: false, + }, +}).plugin(require("mongoose-autopopulate")); + +export const projectSchema = "Projects"; + +export const ProjectDBModel = model(projectSchema, ProjectSchema); diff --git a/server/src/features/projects/models/project_validation_model.ts b/server/src/features/projects/models/project_validation_model.ts new file mode 100644 index 0000000..6bd03d0 --- /dev/null +++ b/server/src/features/projects/models/project_validation_model.ts @@ -0,0 +1,9 @@ +import { IsArray, IsString } from "class-validator"; + +export class ProjectValidationModel { + @IsArray() + public pipelines: [string]; + + @IsString() + public description: string; +} diff --git a/server/src/features/projects/projects_presentation.ts b/server/src/features/projects/projects_presentation.ts index e9414ad..253bf2c 100644 --- a/server/src/features/projects/projects_presentation.ts +++ b/server/src/features/projects/projects_presentation.ts @@ -1,15 +1,12 @@ -// import { TriggerDBModel, TriggerModel } from "./trigger_model"; import { CrudController } from "../../core/controllers/crud_controller"; -import { ProjectDBModel, ProjectModel } from "./projects_model"; +import { ProjectDBModel } from "./models/project_database_model"; +import { ProjectValidationModel } from "./models/project_validation_model"; -export class ProjectsPresentation extends CrudController< - ProjectModel, - typeof ProjectDBModel -> { +export class ProjectsPresentation extends CrudController { constructor() { super({ url: "project", - validationModel: ProjectModel, + validationModel: ProjectValidationModel, databaseModel: ProjectDBModel, }); } diff --git a/server/src/features/realtime/domain/pipeline_status_usecase.ts b/server/src/features/realtime/domain/pipeline_status_usecase.ts new file mode 100644 index 0000000..96422c2 --- /dev/null +++ b/server/src/features/realtime/domain/pipeline_status_usecase.ts @@ -0,0 +1,19 @@ +import { Result } from "../../../core/helpers/result"; +import { ActivePipeline } from "../../../core/models/active_pipeline_model"; +import { pipelineRealTimeService } from "../realtime_presentation"; + +export class PipelineStatusUseCase { + async call(): Promise> { + try { + const status = pipelineRealTimeService.status; + if (status.projectUUID !== null) { + return Result.ok(status); + } + if (status.projectUUID === null) { + return Result.error(new Error("pipelineRealTimeService does not have an active project instance")); + } + } catch (error) { + return Result.error(error as Error); + } + } +} diff --git a/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts b/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts new file mode 100644 index 0000000..7fff00e --- /dev/null +++ b/server/src/features/realtime/domain/run_instance_pipeline_usecase.ts @@ -0,0 +1,41 @@ +import { App } from "../../../core/controllers/app"; +import { Result } from "../../../core/helpers/result"; +import { ReadByIdDataBaseModelUseCase } from "../../../core/usecases/read_by_id_database_model_usecase"; +import { UpdateDataBaseModelUseCase } from "../../../core/usecases/update_database_model_usecase"; +import { + IProjectInstanceModel, + ProjectInstanceDbModel, +} from "../../project_instance/models/project_instance_database_model"; +import { RealTimeValidationModel, pipelineRealTimeService } from "../realtime_presentation"; + +export class RunInstancePipelineUseCase { + async call(model: RealTimeValidationModel): Promise> { + const { id } = model; + const readByIdDataBaseModelUseCase = await new ReadByIdDataBaseModelUseCase( + ProjectInstanceDbModel + ).call(id); + + if (readByIdDataBaseModelUseCase.isFailure()) { + return readByIdDataBaseModelUseCase.forward(); + } + + const projectModel = readByIdDataBaseModelUseCase.value; + projectModel.isActive = true; + + const updateDataBaseModelUseCase = await new UpdateDataBaseModelUseCase( + ProjectInstanceDbModel + ).call(projectModel); + + if (updateDataBaseModelUseCase.isFailure()) { + return updateDataBaseModelUseCase.forward(); + } + pipelineRealTimeService.setPipelineDependency( + projectModel.project.pipelines, + App.staticFilesStoreDir() + projectModel.rootDir + "/", + projectModel._id + ); + pipelineRealTimeService.runPipeline(); + + return Result.ok({ status: "ok" }); + } +} diff --git a/server/src/features/realtime/realtime_presentation.ts b/server/src/features/realtime/realtime_presentation.ts new file mode 100644 index 0000000..406ec5d --- /dev/null +++ b/server/src/features/realtime/realtime_presentation.ts @@ -0,0 +1,24 @@ +import { IsString } from "class-validator"; +import { CoreHttpController } from "../../core/controllers/http_controller"; +import { PipelineRealTimeService } from "../../core/services/pipeline_real_time_service"; +import { RunInstancePipelineUseCase } from "./domain/run_instance_pipeline_usecase"; +import { PipelineStatusUseCase } from "./domain/pipeline_status_usecase"; + +export const pipelineRealTimeService = new PipelineRealTimeService(); + +export class RealTimeValidationModel { + @IsString() + public id: string; +} + +export class RealTimePresentation extends CoreHttpController { + constructor() { + super({ + validationModel: RealTimeValidationModel, + url: "realtime", + databaseModel: null, + }); + super.post(new RunInstancePipelineUseCase().call); + super.get(new PipelineStatusUseCase().call); + } +} diff --git a/server/src/features/triggers/models/trigger_database_model.ts b/server/src/features/triggers/models/trigger_database_model.ts new file mode 100644 index 0000000..5da8fea --- /dev/null +++ b/server/src/features/triggers/models/trigger_database_model.ts @@ -0,0 +1,36 @@ +import { Schema, model } from "mongoose"; + +export interface ITriggerModel { + _id?: string; + type: string; + description: string; + value: string[]; +} + +export const TriggerSchema = new Schema({ + type: { + type: String, + require: true, + }, + description: { + type: String, + }, + value: { + type: Array, + require: true, + }, +}); + +export const triggerSchema = "Trigger"; + +export const TriggerDBModel = model(triggerSchema, TriggerSchema); + +export enum TriggerType { + PROCESS = "PROCESS", + FILE = "FILE", +} + +export interface Trigger { + type: TriggerType; + value: string[]; +} diff --git a/server/src/features/triggers/models/trigger_validation_model.ts b/server/src/features/triggers/models/trigger_validation_model.ts new file mode 100644 index 0000000..86b9174 --- /dev/null +++ b/server/src/features/triggers/models/trigger_validation_model.ts @@ -0,0 +1,16 @@ +import { IsArray, IsOptional, IsEnum, IsString } from "class-validator"; +import { ITriggerModel, TriggerType } from "./trigger_database_model"; + +export class TriggerModelValidationModel implements ITriggerModel { + @IsOptional() + public _id: string; + + @IsString() + public description; + + @IsEnum(TriggerType) + public type: TriggerType; + + @IsArray() + public value: string[]; +} diff --git a/server/src/features/triggers/triggers_presentation.ts b/server/src/features/triggers/triggers_presentation.ts index 77b135f..f48f7dd 100644 --- a/server/src/features/triggers/triggers_presentation.ts +++ b/server/src/features/triggers/triggers_presentation.ts @@ -1,14 +1,12 @@ -import { TriggerDBModel, TriggerModel } from "./trigger_model"; import { CrudController } from "../../core/controllers/crud_controller"; +import { TriggerDBModel } from "./models/trigger_database_model"; +import { TriggerModelValidationModel as TriggerValidationMode } from "./models/trigger_validation_model"; -export class TriggerPresentation extends CrudController< - TriggerModel, - typeof TriggerDBModel -> { +export class TriggerPresentation extends CrudController { constructor() { super({ url: "trigger", - validationModel: TriggerModel, + validationModel: TriggerValidationMode, databaseModel: TriggerDBModel, }); } diff --git a/server/src/main.ts b/server/src/main.ts index 3d844be..3b91a44 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,21 +1,12 @@ import "reflect-metadata"; import { App } from "./core/controllers/app"; -import { Routes } from "./core/interfaces/router"; -import { TriggerPresentation } from "./features/triggers/triggers_presentation"; -import { ProjectsPresentation } from "./features/projects/projects_presentation"; -import { PipelinePresentation } from "./features/pipelines/pipeline_presentation"; -import { ProcessPresentation } from "./features/process/process_presentation"; +import { SocketSubscriber } from "./core/controllers/socket_controller"; +import { extensions } from "./core/extensions/extensions"; +import { httpRoutes } from "./core/controllers/routes"; +import { pipelineRealTimeService } from "./features/realtime/realtime_presentation"; +extensions(); -const httpRoutes: Routes[] = [ - new TriggerPresentation(), - new ProjectsPresentation(), - new ProcessPresentation(), - new PipelinePresentation(), -].map((el) => el.call()); +const socketSubscribers = [new SocketSubscriber(pipelineRealTimeService, "realtime")]; -const computedFolder = ""; - -new App(httpRoutes, computedFolder).listen(); - - \ No newline at end of file +new App(httpRoutes, socketSubscribers).listen(); diff --git a/server/test/controllers/crud_controller_test.ts b/server/test/controllers/crud_controller_test.ts new file mode 100644 index 0000000..23b074a --- /dev/null +++ b/server/test/controllers/crud_controller_test.ts @@ -0,0 +1,49 @@ +import { CrudController } from "../../src/core/controllers/crud_controller"; +import { ClassValidatorMocker } from "../../src/core/helpers/class_validator_mocker"; +import { HttpRepository } from "../../src/core/repository/http_repository"; + +function instanceOfObjectAndHaveId(s: any): string { + if (s instanceof Object && "id" in s) { + return s.id; + } + if (s instanceof Object && "_id" in s) { + return s._id; + } + throw Error(`${s} is not instance object or not have property _id`); +} + +export class CrudControllerTest { + controllerTest: CrudController; + httpRepository: HttpRepository; + + constructor(port: number, controller: CrudController) { + this.controllerTest = controller; + this.httpRepository = new HttpRepository(`http://localhost:${port}`); + } + + async call() { + let result = false; + const mockModel = ClassValidatorMocker.create(this.controllerTest.validationModel); + const postRequestBody = await this.httpRepository.jsonRequest(this.controllerTest.mainURL, "POST", mockModel); + + await postRequestBody.map(async (s) => { + const id = instanceOfObjectAndHaveId(s); + const getRequestBody = await this.httpRepository.jsonRequest(this.controllerTest.mainURL, "GET"); + await getRequestBody.map(async (el) => { + if (el instanceof Array) { + const firstElement = el.firstElement(); + const mockModelUpdate = ClassValidatorMocker.create(this.controllerTest.validationModel); + Object.assign(firstElement, mockModelUpdate); + delete firstElement.__v; + const putReqBody = await this.httpRepository.jsonRequest(this.controllerTest.mainURL, "PUT", firstElement); + await putReqBody.map(async () => { + (await this.httpRepository.jsonRequest(this.controllerTest.mainURL + "?id=" + id, "DELETE")).map(() => { + result = true; + }); + }); + } + }); + }); + return result; + } +} diff --git a/server/test/core/test_core.ts b/server/test/core/test_core.ts index 8ca7373..f3424ff 100644 --- a/server/test/core/test_core.ts +++ b/server/test/core/test_core.ts @@ -1,12 +1,14 @@ import mongoose from "mongoose"; -import { delay } from "../../src/core/helper/delay"; -import { Result } from "../../src/core/helper/result"; -import { TypedEvent } from "../../src/core/helper/typed_event"; +import { delay } from "../../src/core/helpers/delay"; +import { Result } from "../../src/core/helpers/result"; +import { TypedEvent } from "../../src/core/helpers/typed_event"; +import { DropDataBaseUseCase } from "../../src/core/usecases/drop_database_usecase"; +import { ExitAppUseCase } from "../../src/core/usecases/exit_app_usecase"; - -export const before = async () =>{ - await mongoose.connection.dropDatabase() -} +export const before = async () => { + new DropDataBaseUseCase().call(); + new ExitAppUseCase().call(); +}; export class TestCore { allTests = 0; @@ -33,17 +35,14 @@ export class TestCore { console.log("\x1b[32m", "============="); if (this.allTests - this.testOk === 0) { - console.log( - "\x1b[32m", - `✅ All test success! ${this.allTests}/${this.testOk}` - ); + 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}`); } - await before() + await before(); }; resultTest = async ( eventClass: TypedEvent> | any, @@ -54,24 +53,20 @@ export class TestCore { ) => { 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; - } + 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/mocks/log_code.ts b/server/test/mocks/log_code.ts index 2ab9c3c..e8a9b5e 100644 --- a/server/test/mocks/log_code.ts +++ b/server/test/mocks/log_code.ts @@ -1,17 +1,15 @@ - - -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 +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); diff --git a/server/test/mocks/long_code.ts b/server/test/mocks/long_code.ts index d257418..427bbf4 100644 --- a/server/test/mocks/long_code.ts +++ b/server/test/mocks/long_code.ts @@ -1,4 +1,4 @@ -const seconds = 1000 * 10 -setTimeout(()=>{ - console.log(200) -}, seconds) \ No newline at end of file +const seconds = 1000 * 10; +setTimeout(() => { + console.log("long code"); +}, seconds); diff --git a/server/test/model/mock_pipelines.ts b/server/test/model/mock_pipelines.ts new file mode 100644 index 0000000..3dd865d --- /dev/null +++ b/server/test/model/mock_pipelines.ts @@ -0,0 +1,43 @@ +import { EXEC_TYPE } from "../../src/core/models/exec_error_model"; +import { IPipeline, IssueType, StackGenerateType } from "../../src/core/models/process_model"; +import { TriggerType } from "../../src/features/triggers/models/trigger_database_model"; + +export const mockSimplePipeline: IPipeline[] = [ + { + process: { + type: EXEC_TYPE.EXEC, + command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ + "filesMeta":[ + {"type":"folder","name":"example", "path": null,"rewrite":true} + ], + "path":"$PATH" + }'`, + isGenerating: true, + isLocaleCode: false, + issueType: IssueType.WARNING, + }, + trigger: { + type: TriggerType.PROCESS, + value: [""], + }, + env: null, + stackGenerateType: StackGenerateType.SINGLETON, + }, + { + process: { + type: EXEC_TYPE.EXEC, + command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ + "filesMeta":[ + {"type":"file","name":"1.txt", "path":"example","rewrite":true} + ], + "path":"$PATH" + }'`, + isGenerating: true, + isLocaleCode: false, + issueType: IssueType.WARNING, + }, + trigger: null, + env: null, + stackGenerateType: StackGenerateType.SINGLETON, + }, +]; diff --git a/server/test/model/test_db_mongo_model.ts b/server/test/model/test_db_mongo_model.ts index 5e2f708..b00ad42 100644 --- a/server/test/model/test_db_mongo_model.ts +++ b/server/test/model/test_db_mongo_model.ts @@ -1,6 +1,6 @@ import { Schema, model } from "mongoose"; -export interface ITestModel{ +export interface ITestModel { _id?: string; result: string; } @@ -12,4 +12,3 @@ export const TestSchema = new Schema({ const schema = "Test"; export const TestDBModel = model(schema, TestSchema); - \ No newline at end of file diff --git a/server/test/services/executor_program_service_test.ts b/server/test/services/executor_program_service_test.ts index 8cf3849..195563f 100644 --- a/server/test/services/executor_program_service_test.ts +++ b/server/test/services/executor_program_service_test.ts @@ -1,6 +1,6 @@ -import { delay } from "../../src/core/helper/delay"; -import { EXEC_TYPE } from "../../src/core/model/exec_error_model"; -import { ExecutorResult } from "../../src/core/model/executor_result"; +import { delay } from "../../src/core/helpers/delay"; +import { EXEC_TYPE } from "../../src/core/models/exec_error_model"; +import { ExecutorResult } from "../../src/core/models/executor_result"; import { ExecutorProgramService } from "../../src/core/services/executor_program_service"; import { TestCore } from "../core/test_core"; import { resultTest as resultTest, dirname__ } from "../test"; @@ -15,12 +15,8 @@ export class ExecutorProgramServiceTest extends ExecutorProgramService { await this.logWriteAndEventEndTypeSpawn(); }; private async logWriteAndEventEndTypeSpawn() { - const executorProgramService = await new ExecutorProgramService( - dirname__ + "/" - ); - executorProgramService.call(EXEC_TYPE.SPAWN, "node", [ - "./mocks/log_code", - ]); + const executorProgramService = await new ExecutorProgramService(dirname__ + "/"); + executorProgramService.call(EXEC_TYPE.SPAWN, "node", ["./mocks/log_code"]); const test = TestCore.instance; let testIsOk = false; let logEvent = false; @@ -29,24 +25,17 @@ export class ExecutorProgramServiceTest extends ExecutorProgramService { if (e.isSuccess()) { const executorResult = e.value as ExecutorResult; if (logEvent == false) { - logEvent = - executorResult.data != null && executorResult.data != undefined; + 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" - ); + 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" - ); + executorProgramService.call(EXEC_TYPE.EXEC, "node ./test/mocks/log_code"); const test = TestCore.instance; executorProgramService.on((e) => { if (e.isSuccess()) { @@ -61,10 +50,7 @@ export class ExecutorProgramServiceTest extends ExecutorProgramService { } private async longTimeCancelTest() { const executorProgramService = await new ExecutorProgramService("", 1000); - executorProgramService.call( - EXEC_TYPE.EXEC, - "node ./test/mocks/long_code" - ); + executorProgramService.call(EXEC_TYPE.EXEC, "node ./test/mocks/long_code"); await delay(1500); const worker = executorProgramService.worker as Worker; const test = TestCore.instance; diff --git a/server/test/services/files_change_notifier_service_test.ts b/server/test/services/files_change_notifier_service_test.ts index 8246820..8fb1c92 100644 --- a/server/test/services/files_change_notifier_service_test.ts +++ b/server/test/services/files_change_notifier_service_test.ts @@ -1,8 +1,8 @@ import * as fs from "fs"; import { FilesChangeNotifierService } from "../../src/core/services/files_change_notifier_service"; -import { EventsFileChanger } from "../../src/core/model/meta_data_file_manager_model"; +import { EventsFileChanger } from "../../src/core/models/meta_data_file_manager_model"; import { assert, dirname__ } from "../test"; -import { delay } from "../../src/core/helper/delay"; +import { delay } from "../../src/core/helpers/delay"; export class FilesChangerTest extends FilesChangeNotifierService { directory = dirname__ + "/context/"; @@ -26,10 +26,7 @@ export class FilesChangerTest extends FilesChangeNotifierService { await delay(2000); fs.writeFileSync(this.filePath, this.data()); await delay(1000); - this.hashUnitEqualTo( - EventsFileChanger.create, - "FilesChangeNotifierService create file" - ); + this.hashUnitEqualTo(EventsFileChanger.create, "FilesChangeNotifierService create file"); this.cancel(); } @@ -39,28 +36,19 @@ export class FilesChangerTest extends FilesChangeNotifierService { await delay(1000); fs.writeFileSync(this.filePath, this.data() + "132"); await delay(500); - this.hashUnitEqualTo( - EventsFileChanger.update, - "FilesChangeNotifierService update file" - ); + this.hashUnitEqualTo(EventsFileChanger.update, "FilesChangeNotifierService update file"); this.cancel(); } public async initFile() { this.init(); await delay(500); - this.hashUnitEqualTo( - EventsFileChanger.static, - "FilesChangeNotifierService init file" - ); + 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.hashUnitEqualTo(EventsFileChanger.delete, "FilesChangeNotifierService delete file"); this.cancel(); } public async notExistsDirectory() { diff --git a/server/test/services/pipeline_real_time_service_test.ts b/server/test/services/pipeline_real_time_service_test.ts new file mode 100644 index 0000000..7aa28a1 --- /dev/null +++ b/server/test/services/pipeline_real_time_service_test.ts @@ -0,0 +1,12 @@ +import { PipelineRealTimeService } from "../../src/core/services/pipeline_real_time_service"; +import { mockSimplePipeline } from "../model/mock_pipelines"; +import { dirname__ } from "../test"; + +export class PipelineRealTimeServiceTest extends PipelineRealTimeService { + constructor() { + super(); + } + async test() { + // this.runPipeline(mockSimplePipeline, dirname__, ""); + } +} diff --git a/server/test/services/stack_service_test.ts b/server/test/services/stack_service_test.ts index 12a25f8..80d0bcc 100644 --- a/server/test/services/stack_service_test.ts +++ b/server/test/services/stack_service_test.ts @@ -1,97 +1,21 @@ import { rmSync } from "fs"; -import * as fs from "fs"; - -import { - IssueType, - StackGenerateType, -} from "../../src/core/model/process_model"; -import { EXEC_TYPE } from "../../src/core/model/exec_error_model"; import { StackService } from "../../src/core/services/stack_service"; -import { delay } from "../../src/core/helper/delay"; +import { delay } from "../../src/core/helpers/delay"; import { assert, dirname__ } from "../test"; -import { TriggerType } from "../../src/features/triggers/trigger_model"; +import { mockSimplePipeline } from "../model/mock_pipelines"; +import { readDirRecursive } from "../../src/core/repository/fs"; abstract class IStackServiceTest { abstract test(): Promise; } -export function readDirRecursive(path: string, filesToDir: string[] = []) { - const files = fs.readdirSync(path); - files.forEach((file) => { - let filePath = ""; - if (path[path.length - 1] !== "/") { - filePath = `${path}/${file}`; - } else { - filePath = `${path}${file}`; - } - - const stats = fs.statSync(filePath); - if (stats.isDirectory()) { - readDirRecursive(filePath, filesToDir); - } else { - filesToDir.push(file); - } - }); - return filesToDir; -} - -class SimpleTestStackServiceTest - extends StackService - implements IStackServiceTest -{ +class SimpleTestStackServiceTest extends StackService implements IStackServiceTest { constructor() { - super( - [ - { - process: { - type: EXEC_TYPE.EXEC, - command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ - "filesMeta":[ - {"type":"folder","name":"example", "path": null,"rewrite":true} - ], - "path":"$PATH" - }'`, - isGenerating: true, - isLocaleCode: false, - issueType: IssueType.WARNING, - }, - trigger: { - type: TriggerType.FILE, - value: ["context"], - }, - env: null, - stackGenerateType: StackGenerateType.SINGLETON, - }, - { - process: { - type: EXEC_TYPE.EXEC, - command: `nix run gitlab:robossembler/nix-robossembler-overlay#test-script '{ - "filesMeta":[ - {"type":"file","name":"1.txt", "path":"example","rewrite":true} - ], - "path":"$PATH" - }'`, - isGenerating: true, - isLocaleCode: false, - issueType: IssueType.WARNING, - }, - trigger: { - type: TriggerType.FILE, - value: ["1.txt"], - }, - env: null, - stackGenerateType: StackGenerateType.SINGLETON, - }, - ], - dirname__ + "/context/" - ); + super(mockSimplePipeline, dirname__ + "/context/"); } async test(): Promise { await this.call(); - const testResult = readDirRecursive(this.path).equals( - ["1.txt", "test.txt"], - true - ); + const testResult = readDirRecursive(this.path).equals(["1.txt", "test.txt"], true); await delay(100); rmSync(this.path + "example/", { recursive: true }); return testResult; diff --git a/server/test/services/trigger_service_test.ts b/server/test/services/trigger_service_test.ts index d919256..970168c 100644 --- a/server/test/services/trigger_service_test.ts +++ b/server/test/services/trigger_service_test.ts @@ -1,10 +1,7 @@ -import { - EventsFileChanger, - MetaDataFileManagerModel, -} from "../../src/core/model/meta_data_file_manager_model"; - +import { EventsFileChanger, MetaDataFileManagerModel } from "../../src/core/models/meta_data_file_manager_model"; + import { TriggerService } from "../../src/core/services/trigger_service"; -import { TriggerType } from "../../src/features/triggers/trigger_model"; +import { TriggerType } from "../../src/features/triggers/models/trigger_database_model"; import { assert } from "../test"; abstract class TriggerTest { abstract test(): Promise; @@ -17,11 +14,7 @@ class TriggerServiceFileOkTest extends TriggerService implements TriggerTest { value: ["context"], }, { - "/context/": new MetaDataFileManagerModel( - "", - "", - EventsFileChanger.create - ), + "/context/": new MetaDataFileManagerModel("", "", EventsFileChanger.create), }, "" ); @@ -32,10 +25,7 @@ class TriggerServiceFileOkTest extends TriggerService implements TriggerTest { return r.isSuccess(); } } -class TriggerServiceFileErrorTest - extends TriggerService - implements TriggerTest -{ +class TriggerServiceFileErrorTest extends TriggerService implements TriggerTest { constructor() { super( { @@ -44,11 +34,7 @@ class TriggerServiceFileErrorTest }, { "/ctx/": new MetaDataFileManagerModel("", "", EventsFileChanger.create), - "/context/": new MetaDataFileManagerModel( - "", - "", - EventsFileChanger.create - ), + "/context/": new MetaDataFileManagerModel("", "", EventsFileChanger.create), }, "" @@ -60,10 +46,7 @@ class TriggerServiceFileErrorTest return r.isFailure(); } } -class TriggerServiceProcessOkTest - extends TriggerService - implements TriggerTest -{ +class TriggerServiceProcessOkTest extends TriggerService implements TriggerTest { constructor() { super( { @@ -77,11 +60,7 @@ class TriggerServiceProcessOkTest ], }, { - "/context/": new MetaDataFileManagerModel( - "", - "", - EventsFileChanger.create - ), + "/context/": new MetaDataFileManagerModel("", "", EventsFileChanger.create), }, "" ); @@ -92,10 +71,7 @@ class TriggerServiceProcessOkTest } } -class TriggerServiceProcessErrorTest - extends TriggerService - implements TriggerTest -{ +class TriggerServiceProcessErrorTest extends TriggerService implements TriggerTest { constructor() { super( { @@ -109,11 +85,7 @@ class TriggerServiceProcessErrorTest ], }, { - "/context/": new MetaDataFileManagerModel( - "", - "", - EventsFileChanger.create - ), + "/context/": new MetaDataFileManagerModel("", "", EventsFileChanger.create), }, "" ); diff --git a/server/test/test.ts b/server/test/test.ts index f29630a..1bbe197 100644 --- a/server/test/test.ts +++ b/server/test/test.ts @@ -1,51 +1,84 @@ +import "reflect-metadata"; import { TestCore } from "./core/test_core"; -import { UnitTestEnv } from "../src/core/di/env"; +// import { UnitTestEnv } from "../src/core/di/env"; import { dirname } from "path"; -import locator from "../src/core/di/register_di"; +// import locator from "../src/core/di/register_di"; import { ExecutorProgramServiceTest } from "./services/executor_program_service_test"; import { FilesChangerTest } from "./services/files_change_notifier_service_test"; import { TriggerServiceTest } from "./services/trigger_service_test"; import { StackServiceTest } from "./services/stack_service_test"; -import mongoose from "mongoose"; import { CreateDataBaseModelUseCaseTest } from "./usecases/create_database_model_usecase_test"; import { DeleteDataBaseModelUseCaseTest } from "./usecases/delete_database_model_usecase_test"; import { ReadDataBaseModelUseCaseTest } from "./usecases/read_database_model_usecase_test"; import { UpdateDataBaseModelUseCaseTest } from "./usecases/update_database_model_usecase"; import { PaginationDataBaseModelUseCaseTest } from "./usecases/pagination_database_model_usecase_test"; +import { extensions } from "../src/core/extensions/extensions"; +import { CrudControllerTest } from "./controllers/crud_controller_test"; +import { TriggerPresentation } from "../src/features/triggers/triggers_presentation"; +import { App, Environment, ServerStatus } from "../src/core/controllers/app"; +import { httpRoutes } from "../src/core/controllers/routes"; +import { DataBaseConnectUseCase } from "../src/core/usecases/database_connect_usecase"; - const testCore = TestCore.instance; - export const dirname__: string = dirname(__filename); export const assert = testCore.assert; export const resultTest = testCore.resultTest; -const env = new UnitTestEnv(dirname__); -locator(env); +const tests = [ + CreateDataBaseModelUseCaseTest, + DeleteDataBaseModelUseCaseTest, + ReadDataBaseModelUseCaseTest, + UpdateDataBaseModelUseCaseTest, + PaginationDataBaseModelUseCaseTest, +]; +const init = async () => { + await new DataBaseConnectUseCase().call(); +}; -const tests = [CreateDataBaseModelUseCaseTest, DeleteDataBaseModelUseCaseTest,ReadDataBaseModelUseCaseTest,UpdateDataBaseModelUseCaseTest, PaginationDataBaseModelUseCaseTest] -const init = async () =>{ - await mongoose.connect('mongodb://127.0.0.1:27017/test') -} - -const test = async () =>{ +const unitTest = async () => { + await init(); await new ExecutorProgramServiceTest(dirname__).test(); await new FilesChangerTest(dirname__).test(); await new StackServiceTest(dirname__ + "/context/").test(); await new TriggerServiceTest().test(); - await new CreateDataBaseModelUseCaseTest().test() - - await new CreateDataBaseModelUseCaseTest().test() - await new DeleteDataBaseModelUseCaseTest().test() - await new ReadDataBaseModelUseCaseTest().test() - await new UpdateDataBaseModelUseCaseTest().test() + await new CreateDataBaseModelUseCaseTest().test(); + await new CreateDataBaseModelUseCaseTest().test(); + await new DeleteDataBaseModelUseCaseTest().test(); + await new ReadDataBaseModelUseCaseTest().test(); + await new UpdateDataBaseModelUseCaseTest().test(); + // await new PipelineRealTimeServiceTest().test() for await (const usecase of tests) { - testCore.assert(await new usecase().test(), usecase.name) + testCore.assert(await new usecase().test(), usecase.name); } -} +}; +const presentationCrudControllers = [new TriggerPresentation()]; +const e2eTest = async () => { + const app = new App(httpRoutes, [], Environment.E2E_TEST); + app.listen(); + await new Promise((resolve, reject) => { + app.on(async (e) => { + if (e === ServerStatus.finished) { + for await (const el of presentationCrudControllers) { + testCore.assert(await new CrudControllerTest(app.port, el).call(), el.constructor.name); + } + resolve(e); + } + if (e === ServerStatus.error) { + console.log(e); + reject(e); + } + }); + }); +}; const main = async () => { - await init() - await test() + extensions(); + if (process.env.NODE_ENV === "unit") { + await unitTest(); + } + + if (process.env.NODE_ENV === "e2e") { + await e2eTest(); + } await testCore.testResult(); }; diff --git a/server/test/usecases/pagination_database_model_usecase_test.ts b/server/test/usecases/pagination_database_model_usecase_test.ts index 48f3433..9c5878d 100644 --- a/server/test/usecases/pagination_database_model_usecase_test.ts +++ b/server/test/usecases/pagination_database_model_usecase_test.ts @@ -5,9 +5,7 @@ export class PaginationDataBaseModelUseCaseTest { async test() { let testIsSuccess = false; await ( - await new PaginationDataBaseModelUseCase(TestDBModel, 1).call( - 1 - ) + await new PaginationDataBaseModelUseCase(TestDBModel, 1).call(1) ).fold( (s) => { testIsSuccess = s.length === 1; diff --git a/server/test/usecases/read_database_model_usecase_test.ts b/server/test/usecases/read_database_model_usecase_test.ts index 94fae6e..f3c5962 100644 --- a/server/test/usecases/read_database_model_usecase_test.ts +++ b/server/test/usecases/read_database_model_usecase_test.ts @@ -1,25 +1,20 @@ import { CreateDataBaseModelUseCase } from "../../src/core/usecases/create_database_model_usecase"; -import { ReadByIdDataBaseModelUseCase } from "../../src/core/usecases/read_database_model_usecase"; +import { ReadByIdDataBaseModelUseCase } from "../../src/core/usecases/read_by_id_database_model_usecase"; import { ITestModel, TestDBModel } from "../model/test_db_mongo_model"; export class ReadDataBaseModelUseCaseTest { async test() { let testIsSuccess = false; - const result = await new CreateDataBaseModelUseCase( - TestDBModel - ).call({ + const result = await new CreateDataBaseModelUseCase(TestDBModel).call({ result: "test", }); await result.fold( async (s) => { - const r = await new ReadByIdDataBaseModelUseCase( - TestDBModel - ).call(s.id); + const r = await new ReadByIdDataBaseModelUseCase(TestDBModel).call(s.id); await r.fold( (_s1) => { testIsSuccess = true; - }, (_e) => {} ); diff --git a/server/test/usecases/update_database_model_usecase.ts b/server/test/usecases/update_database_model_usecase.ts index 868fec4..f5d722b 100644 --- a/server/test/usecases/update_database_model_usecase.ts +++ b/server/test/usecases/update_database_model_usecase.ts @@ -6,18 +6,14 @@ export class UpdateDataBaseModelUseCaseTest { async test() { let testIsSuccess = false; - const model = await new CreateDataBaseModelUseCase( - TestDBModel - ).call({ + const model = await new CreateDataBaseModelUseCase(TestDBModel).call({ result: "test", }); await model.fold( async (s) => { ( - await new UpdateDataBaseModelUseCase( - TestDBModel - ).call({ - _id:s.id, + await new UpdateDataBaseModelUseCase(TestDBModel).call({ + _id: s.id, result: "complete", }) ).fold( diff --git a/server/tsconfig.json b/server/tsconfig.json index f5e853d..df616d5 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,19 +1,19 @@ { - "compileOnSave": false, - "compilerOptions": { - "target": "es2017", - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "pretty": true, - "declaration": true, - "outDir": "./build", - "allowJs": true, - "noEmit": false, - "esModuleInterop": true, - "resolveJsonModule": true, - "importHelpers": true, - } - } \ No newline at end of file + "compileOnSave": false, + "compilerOptions": { + "target": "es2017", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "pretty": true, + "declaration": true, + "outDir": "./build", + "allowJs": true, + "noEmit": false, + "esModuleInterop": true, + "resolveJsonModule": true, + "importHelpers": true + } +} diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..309bb04 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +package-lock.json +todo.md \ No newline at end of file diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..f03a316 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,62 @@ +{ + "name": "ui-robossembler", + "version": "0.1.0", + "private": true, + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@types/jest": "^27.5.2", + "@types/node": "^16.18.46", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@types/socket.io-client": "^3.0.0", + "@types/uuid": "^9.0.2", + "formik-antd": "^2.0.4", + "i18next": "^23.6.0", + "mobx": "^6.10.0", + "mobx-react-lite": "^4.0.4", + "mobx-store-inheritance": "^1.0.6", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^13.3.1", + "react-infinite-scroll-component": "^6.1.0", + "react-router-dom": "^6.18.0", + "react-scripts": "5.0.1", + "sass": "^1.66.1", + "socket.io-client": "^4.7.2", + "three": "^0.159.0", + "typescript": "^4.9.5", + "urdf-loader": "^0.12.1", + "uuid": "^9.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "dev": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/three": "^0.158.3" + } +} diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/ui/public/favicon.ico differ diff --git a/ui/public/index.html b/ui/public/index.html new file mode 100644 index 0000000..2371e57 --- /dev/null +++ b/ui/public/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + robossembler: pipeline + + + + +
+ + + + \ No newline at end of file diff --git a/ui/public/logo192.png b/ui/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/ui/public/logo192.png differ diff --git a/ui/public/logo512.png b/ui/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/ui/public/logo512.png differ diff --git a/ui/public/manifest.json b/ui/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/ui/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/ui/public/robots.txt b/ui/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/ui/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/ui/src/core/assets/icons/add.svg b/ui/src/core/assets/icons/add.svg new file mode 100644 index 0000000..acc2726 --- /dev/null +++ b/ui/src/core/assets/icons/add.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/core/assets/icons/check.svg b/ui/src/core/assets/icons/check.svg new file mode 100644 index 0000000..d8c2fc2 --- /dev/null +++ b/ui/src/core/assets/icons/check.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/core/assets/icons/chevron.svg b/ui/src/core/assets/icons/chevron.svg new file mode 100644 index 0000000..b28d728 --- /dev/null +++ b/ui/src/core/assets/icons/chevron.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/core/assets/icons/delete.svg b/ui/src/core/assets/icons/delete.svg new file mode 100644 index 0000000..d15de60 --- /dev/null +++ b/ui/src/core/assets/icons/delete.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/core/assets/icons/error.svg b/ui/src/core/assets/icons/error.svg new file mode 100644 index 0000000..3f02c89 --- /dev/null +++ b/ui/src/core/assets/icons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/core/assets/icons/left_icon.svg b/ui/src/core/assets/icons/left_icon.svg new file mode 100644 index 0000000..cf4c2af --- /dev/null +++ b/ui/src/core/assets/icons/left_icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/core/assets/icons/reload.svg b/ui/src/core/assets/icons/reload.svg new file mode 100644 index 0000000..2bdbecc --- /dev/null +++ b/ui/src/core/assets/icons/reload.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ui/src/core/assets/images/logo.svg b/ui/src/core/assets/images/logo.svg new file mode 100644 index 0000000..58b91d4 --- /dev/null +++ b/ui/src/core/assets/images/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/core/extensions/array.ts b/ui/src/core/extensions/array.ts new file mode 100644 index 0000000..4431efc --- /dev/null +++ b/ui/src/core/extensions/array.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-this-alias */ +export const ArrayExtensions = () => { + if ([].equals === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.equals = function (array, strict = true) { + if (!array) return false; + + if (arguments.length === 1) strict = true; + + if (this.length !== array.length) return false; + + for (let i = 0; i < this.length; i++) { + if (this[i] instanceof Array && array[i] instanceof Array) { + if (!this[i].equals(array[i], strict)) return false; + } else if (strict && this[i] !== array[i]) { + return false; + } else if (!strict) { + return this.sort().equals(array.sort(), true); + } + } + return true; + }; + } + if ([].lastElement === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.lastElement = function () { + const instanceCheck = this; + if (instanceCheck === undefined) { + return undefined; + } else { + const instance = instanceCheck as []; + return instance[instance.length - 1]; + } + }; + } + if ([].isEmpty === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.isEmpty = function () { + return this.length === 0; + }; + } + if ([].isNotEmpty === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.isNotEmpty = function () { + return this.length !== 0; + }; + } + if ([].hasIncludeElement === undefined) { + // eslint-disable-next-line no-extend-native + Array.prototype.hasIncludeElement = function (element) { + return this.indexOf(element) !== -1; + }; + } +}; diff --git a/ui/src/core/extensions/extensions.ts b/ui/src/core/extensions/extensions.ts new file mode 100644 index 0000000..5723394 --- /dev/null +++ b/ui/src/core/extensions/extensions.ts @@ -0,0 +1,27 @@ +import { ArrayExtensions } from "./array"; +import { MapExtensions } from "./map"; +import { StringExtensions } from "./string"; + +export type CallBackFunction = (value: T) => void; + +declare global { + interface Array { + // @strict: The parameter is determined whether the arrays must be exactly the same in content and order of this relationship or simply follow the same requirements. + equals(array: Array, strict: boolean): boolean; + lastElement(): T | undefined; + isEmpty(): boolean; + isNotEmpty(): boolean; + hasIncludeElement(element: T): boolean; + } + interface String { + isEmpty(): boolean; + } + interface Map { + addValueOrMakeCallback(key: K, value: V, callBack: CallBackFunction): void; + } +} +export const extensions = () => { + ArrayExtensions(); + StringExtensions(); + MapExtensions(); +}; diff --git a/ui/src/core/extensions/map.ts b/ui/src/core/extensions/map.ts new file mode 100644 index 0000000..22fd3e7 --- /dev/null +++ b/ui/src/core/extensions/map.ts @@ -0,0 +1,15 @@ +export const MapExtensions = () => { + if (Map.prototype.addValueOrMakeCallback === undefined) { + // eslint-disable-next-line no-extend-native + Map.prototype.addValueOrMakeCallback = function (key, value, fn) { + if (this.has(key)) { + this.set(key, value); + fn(this); + return; + } else { + this.set(key, value); + } + }; + } +}; +Object(); diff --git a/ui/src/core/extensions/string.ts b/ui/src/core/extensions/string.ts new file mode 100644 index 0000000..030607e --- /dev/null +++ b/ui/src/core/extensions/string.ts @@ -0,0 +1,8 @@ +export const StringExtensions = () => { + if ("".isEmpty === undefined) { + // eslint-disable-next-line no-extend-native + String.prototype.isEmpty = function () { + return this.length === 0; + }; + } +}; diff --git a/server/src/core/helper/result.ts b/ui/src/core/helper/result.ts similarity index 99% rename from server/src/core/helper/result.ts rename to ui/src/core/helper/result.ts index 1f24112..bdfc37c 100644 --- a/server/src/core/helper/result.ts +++ b/ui/src/core/helper/result.ts @@ -193,6 +193,7 @@ type PromiseReturnType any> = T extends ( ? U : never; +// eslint-disable-next-line @typescript-eslint/no-redeclare export namespace Result { export function ok< ErrorType extends unknown, @@ -392,6 +393,7 @@ abstract class Base< onSuccess: (value: OkType) => R, onFailure: (error: ErrorType) => R ): R; + fold( onSuccess: (value: OkType) => Promise, onFailure: (error: ErrorType) => Promise diff --git a/ui/src/core/helper/typed_event.ts b/ui/src/core/helper/typed_event.ts new file mode 100644 index 0000000..e7002c3 --- /dev/null +++ b/ui/src/core/helper/typed_event.ts @@ -0,0 +1,42 @@ +export interface Listener { + (event: T): any; +} + +export interface Disposable { + dispose(): void; +} + +export class TypedEvent { + private listeners: Listener[] = []; + public listenersOnces: Listener[] = []; + + on = (listener: Listener): Disposable => { + this.listeners.push(listener); + return { + dispose: () => this.off(listener), + }; + }; + + once = (listener: Listener): void => { + this.listenersOnces.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.listenersOnces.length > 0) { + const toCall = this.listenersOnces; + this.listenersOnces = []; + toCall.forEach((listener) => listener(event)); + } + }; + + pipe = (te: TypedEvent): Disposable => { + return this.on((e) => te.emit(e)); + }; +} diff --git a/ui/src/core/helper/validate.ts b/ui/src/core/helper/validate.ts new file mode 100644 index 0000000..f570767 --- /dev/null +++ b/ui/src/core/helper/validate.ts @@ -0,0 +1,3 @@ +export function validateRequired(value: string) { + return value ? undefined : "required"; +} diff --git a/ui/src/core/hook/key_listner.tsx b/ui/src/core/hook/key_listner.tsx new file mode 100644 index 0000000..7c7bb60 --- /dev/null +++ b/ui/src/core/hook/key_listner.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +export const useKeyLister = (fn: Function) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const pressed = new Map(); + + const registerKeyPress = React.useCallback( + (event: KeyboardEvent, codes: string[], callBack: Function) => { + if (codes.hasIncludeElement(event.code)) { + pressed.addValueOrMakeCallback(event.code, event.type, (e) => { + if (Array.from(pressed.values()).equals(["keydown", "keydown"], false)) { + callBack(); + } + }); + } + }, + [pressed] + ); + + React.useEffect(() => { + window.addEventListener("keyup", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => fn)); + window.addEventListener("keydown", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => {})); + }, [fn, registerKeyPress]); + + return []; +}; diff --git a/ui/src/core/l10n/i18n.ts b/ui/src/core/l10n/i18n.ts new file mode 100644 index 0000000..5251bfe --- /dev/null +++ b/ui/src/core/l10n/i18n.ts @@ -0,0 +1,15 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import { resources } from "./locale"; +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + resources, + lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); diff --git a/ui/src/core/l10n/locale.ts b/ui/src/core/l10n/locale.ts new file mode 100644 index 0000000..9172023 --- /dev/null +++ b/ui/src/core/l10n/locale.ts @@ -0,0 +1,12 @@ +export const resources = { + en: { + translation: { + "Welcome to React": "Welcome to React and react-i18next", + }, + }, + fr: { + translation: { + "Welcome to React": "Bienvenue à React et react-i18next", + }, + }, +}; diff --git a/ui/src/core/model/active_pipiline.ts b/ui/src/core/model/active_pipiline.ts new file mode 100644 index 0000000..2079613 --- /dev/null +++ b/ui/src/core/model/active_pipiline.ts @@ -0,0 +1,7 @@ + +export interface ActivePipeline { + pipelineIsRunning: boolean; + projectUUID?: string | null; + lastProcessCompleteCount: number | null; + error: any; +} diff --git a/ui/src/core/model/database_model.ts b/ui/src/core/model/database_model.ts new file mode 100644 index 0000000..d2e7f7a --- /dev/null +++ b/ui/src/core/model/database_model.ts @@ -0,0 +1,3 @@ +export interface DatabaseModel { + _id?: string; +} diff --git a/ui/src/core/model/trigger_model.ts b/ui/src/core/model/trigger_model.ts new file mode 100644 index 0000000..5ea29e5 --- /dev/null +++ b/ui/src/core/model/trigger_model.ts @@ -0,0 +1,12 @@ +import { DatabaseModel } from "./database_model"; + +export interface ITriggerModel extends DatabaseModel { + type: string; + description: string; + value: string[]; +} + +export enum TriggerType { + PROCESS = "PROCESS", + FILE = "FILE", +} diff --git a/ui/src/core/repository/core_there_repository.ts b/ui/src/core/repository/core_there_repository.ts new file mode 100644 index 0000000..82b8da9 --- /dev/null +++ b/ui/src/core/repository/core_there_repository.ts @@ -0,0 +1,189 @@ +import { + DirectionalLight, + Object3D, + PerspectiveCamera, + Scene, + WebGLRenderer, + AmbientLight, + Vector3, + MeshBasicMaterial, + Mesh, + BoxGeometry, + Object3DEventMap, + Box3, + Sphere, + LineBasicMaterial, + EdgesGeometry, + Raycaster, + LineSegments, + Vector2, +} from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; +import { BaseSceneItemModel, StaticAssetItemModel } from "../../features/scene_manager/scene_manager_store"; +import { TypedEvent } from "../helper/typed_event"; +import { Result } from "../helper/result"; + +interface IEmissiveCache { + status: boolean; + object3d: Object3D; +} +export class CoreThereRepository extends TypedEvent { + scene = new Scene(); + camera: PerspectiveCamera; + webGlRender: WebGLRenderer; + htmlCanvasRef: HTMLCanvasElement; + objectEmissive = new Map(); + constructor(htmlCanvasRef: HTMLCanvasElement) { + super(); + const renderer = new WebGLRenderer({ + canvas: htmlCanvasRef as HTMLCanvasElement, + antialias: true, + alpha: true, + }); + const aspectCamera = window.outerWidth / window.outerHeight; + this.camera = new PerspectiveCamera(800, aspectCamera, 0.1, 10000); + this.webGlRender = renderer; + this.htmlCanvasRef = htmlCanvasRef; + this.init(); + } + setRayCastAndGetFirstObject(vector: Vector2): Result { + const raycaster = new Raycaster(); + raycaster.setFromCamera(vector, this.camera); + const intersects = raycaster.intersectObjects(this.scene.children); + if (intersects.length > 0) { + return Result.ok(intersects[0].object.name); + } + + return Result.error(undefined); + } + addCube(num: number, translateTo: string = "X") { + const geometry = new BoxGeometry(1, 1, 1); + const material = new MeshBasicMaterial({ color: 0x00ff00 }); + const cube = new Mesh(geometry, material); + cube.name = "Cube" + String(num); + + eval(`cube.translate${translateTo}(${num * 10})`); + this.scene.add(cube); + } + init() { + const directionalLight = new DirectionalLight(0xffffff, 0.2); + directionalLight.castShadow = true; + directionalLight.position.set(-1, 2, 4); + this.scene.add(directionalLight); + const ambientLight = new AmbientLight(0xffffff, 0.7); + this.scene.add(ambientLight); + + this.addCube(1); + this.addCube(2); + this.addCube(3); + this.addCube(4); + + const onResize = () => { + this.camera.aspect = window.outerWidth / window.outerHeight; + this.camera.updateProjectionMatrix(); + this.webGlRender.setSize(window.outerWidth, window.outerHeight); + }; + window.addEventListener("resize", onResize, false); + new OrbitControls(this.camera, this.htmlCanvasRef); + } + render() { + this.webGlRender.setSize(window.outerWidth, window.outerHeight); + this.webGlRender.setAnimationLoop(() => { + this.webGlRender.render(this.scene, this.camera); + }); + } + getAllSceneModels(): BaseSceneItemModel[] { + return this.getAllSceneNameModels().map((e) => new StaticAssetItemModel(e)); + } + getAllSceneNameModels(): string[] { + return this.scene.children.filter((el) => el.name !== "").map((el) => el.name); + } + getObjectsAtName(name: string): Object3D { + return this.scene.children.filter((el) => el.name === name)[0]; + } + loader(urls: string[], callBack: Function) {} + fitCameraToCenteredObject(objects: string[], offset = 4) { + // https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/ + const boundingBox = new Box3().setFromPoints( + objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position) + ); + + var size = new Vector3(); + boundingBox.getSize(size); + + const fov = this.camera.fov * (Math.PI / 180); + const fovh = 2 * Math.atan(Math.tan(fov / 2) * this.camera.aspect); + let dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)); + let dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)); + let cameraZ = Math.max(dx, dy); + + if (offset !== undefined && offset !== 0) cameraZ *= offset; + + this.camera.position.set(0, 0, cameraZ); + + const minZ = boundingBox.min.z; + const cameraToFarEdge = minZ < 0 ? -minZ + cameraZ : cameraZ - minZ; + + this.camera.far = cameraToFarEdge * 3; + this.camera.updateProjectionMatrix(); + let orbitControls = new OrbitControls(this.camera, this.htmlCanvasRef); + + orbitControls.maxDistance = cameraToFarEdge * 2; + new OrbitControls(this.camera, this.htmlCanvasRef); + } + switchObjectEmissive(name: string) { + const mesh = this.getObjectsAtName(name); + const result = this.objectEmissive.get(mesh.name); + if (result?.status) { + this.scene.remove(mesh); + this.scene.add(result.object3d); + this.objectEmissive.set(mesh.name, { + status: false, + object3d: mesh, + }); + } else { + this.objectEmissive.set(mesh.name, { + status: true, + object3d: mesh, + }); + + if (mesh instanceof Mesh) { + const newMesh = new LineSegments(mesh.geometry, new LineBasicMaterial({ color: 0x000000 })); + newMesh.name = mesh.name; + newMesh.translateX(mesh.position.x); + newMesh.translateY(mesh.position.y); + newMesh.translateZ(mesh.position.z); + this.scene.remove(mesh); + this.scene.add(newMesh); + } + } + } + + fitSelectedObjectToScreen(objects: string[]) { + //https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object + let boundBox = new Box3().setFromPoints(objects.map((el) => this.getObjectsAtName(el)).map((el) => el.position)); + let boundSphere = boundBox.getBoundingSphere(new Sphere()); + let vFoV = this.camera.getEffectiveFOV(); + let hFoV = this.camera.fov * this.camera.aspect; + let FoV = Math.min(vFoV, hFoV); + let FoV2 = FoV / 2; + let dir = new Vector3(); + this.camera.getWorldDirection(dir); + let bsWorld = boundSphere.center.clone(); + let th = (FoV2 * Math.PI) / 180.0; + let sina = Math.sin(th); + let R = boundSphere.radius; + let FL = R / sina; + let cameraDir = new Vector3(); + let cameraOffs = cameraDir.clone(); + cameraOffs.multiplyScalar(-FL); + + let newCameraPos = bsWorld.clone().add(cameraOffs); + + this.camera.translateX(newCameraPos.x); + this.camera.translateY(newCameraPos.y); + this.camera.translateZ(newCameraPos.z); + this.camera.lookAt(bsWorld); + new OrbitControls(this.camera, this.htmlCanvasRef); + } +} diff --git a/ui/src/core/repository/http_repository.ts b/ui/src/core/repository/http_repository.ts new file mode 100644 index 0000000..0ebb2e7 --- /dev/null +++ b/ui/src/core/repository/http_repository.ts @@ -0,0 +1,64 @@ +import { Result } from "../helper/result"; + +export enum HttpMethod { + GET = "GET", + POST = "POST", +} +export class HttpError extends Error { + status: number; + error: any; + constructor(error: any, status: number) { + super(error); + this.error = error; + this.status = status; + } +} + +export class HttpRepository { + private server = "http://localhost:4001"; + + public async jsonRequest( + method: HttpMethod, + url: string, + data?: any + ): Promise> { + try { + const reqInit = { + body: data, + method: method, + headers: { "Content-Type": "application/json" }, + }; + if (data !== undefined) { + reqInit["body"] = JSON.stringify(data); + } + const response = await fetch(this.server + url, reqInit); + + if (response.status !== 200) { + return Result.error(new HttpError(this.server + url, response.status)); + } + + return Result.ok(await response.json()); + } catch (error) { + return Result.error(new HttpError(error, 0)); + } + } + + public async request( + method: HttpMethod, + url: string, + data?: any + ): Promise { + const reqInit = { + body: data, + method: method, + }; + if (data !== undefined) { + reqInit["body"] = data; + } + const response = await fetch(this.server + url, reqInit); + if (response.status !== 200) { + throw new Error(await response.json()); + } + return response.json(); + } +} diff --git a/ui/src/core/repository/socket_repository.ts b/ui/src/core/repository/socket_repository.ts new file mode 100644 index 0000000..6eda6e7 --- /dev/null +++ b/ui/src/core/repository/socket_repository.ts @@ -0,0 +1,14 @@ +import { Socket, io } from "socket.io-client"; + +export class SocketRepository { + serverURL = "ws://localhost:4001"; + socket: Socket | undefined; + async connect() { + const socket = io(this.serverURL); + this.socket = socket; + socket.connect(); + socket.on('realtime', (d) =>{ + console.log(d) + }) + } +} diff --git a/ui/src/core/routers/routers.tsx b/ui/src/core/routers/routers.tsx new file mode 100644 index 0000000..e71ed7b --- /dev/null +++ b/ui/src/core/routers/routers.tsx @@ -0,0 +1,66 @@ +import { createBrowserRouter } from "react-router-dom"; +import { AllProjectScreen, AllProjectScreenPath } from "../../features/all_projects/presentation/all_projects_screen"; +import { + PipelineInstanceScreen, + PipelineInstanceScreenPath, +} from "../../features/pipeline_instance_main_screen/pipeline_instance_screen"; +import { + SelectProjectScreen, + SelectProjectScreenPath, +} from "../../features/select_project/presentation/select_project"; +import { + CreatePipelineScreen, + CreatePipelineScreenPath, +} from "../../features/create_pipeline/presentation/create_pipeline_screen"; +import { CreateProjectScreen, CreateProjectScreenPath } from "../../features/create_project/create_project_screen"; +import { + CreateTriggerScreenPath, + TriggerScreen, +} from "../../features/create_trigger/presentation/create_trigger_screen"; +import { + CreateProcessScreen, + CreateProcessScreenPath, +} from "../../features/create_process/presentation/create_process_screen"; +import { ProjectRepository } from "../../features/all_projects/data/project_repository"; +import { + CreateProjectInstancePath, + CreateProjectInstanceScreen, +} from "../../features/create_project_instance/create_project_instance"; + +const idURL = ":id"; + +export const router = createBrowserRouter([ + { + path: AllProjectScreenPath, + loader: new ProjectRepository().loader, + element: , + }, + { + path: PipelineInstanceScreenPath + idURL, + element: , + }, + { + path: SelectProjectScreenPath, + element: , + }, + { + path: CreatePipelineScreenPath, + element: , + }, + { + path: CreateProjectScreenPath, + element: , + }, + { + path: CreateTriggerScreenPath, + element: , + }, + { + path: CreateProcessScreenPath, + element: , + }, + { + path: CreateProjectInstancePath + idURL, + element: , + }, +]); diff --git a/ui/src/core/store/base_store.ts b/ui/src/core/store/base_store.ts new file mode 100644 index 0000000..825a76e --- /dev/null +++ b/ui/src/core/store/base_store.ts @@ -0,0 +1,22 @@ +// TODO(IDONTSUDO): нужно переписать все запросы под BaseStore + +import { Result } from "../helper/result"; + +export class BaseStore { + isLoading = false; + isError = false; + + async loadingHelper(callBack: Promise>) { + this.isLoading = true; + + const result = await callBack; + if (result.isFailure()) { + this.isError = true; + this.isLoading = false; + return result.forward(); + } + + this.isLoading = false; + return result; + } +} diff --git a/ui/src/core/ui/header/header.tsx b/ui/src/core/ui/header/header.tsx new file mode 100644 index 0000000..a2810a0 --- /dev/null +++ b/ui/src/core/ui/header/header.tsx @@ -0,0 +1,94 @@ +import * as React from "react"; +import { Typography } from "antd"; +import { Col, Row } from "antd"; +import { LinkTypography } from "../link/link"; +import { ReactComponent as LeftIcon } from "../../assets/icons/left_icon.svg"; +import { useNavigate } from "react-router-dom"; + +const { Title } = Typography; + +export interface IHeader { + largeText?: string; + minText?: string; + path?: string; + needBackButton?: undefined | any; +} + +export const Header: React.FunctionComponent = (props: IHeader) => { + const navigate = useNavigate(); + const needBackButton = props.needBackButton !== undefined ? false : true; + + return ( + + + {needBackButton ? ( + <> +
{ + navigate(-1); + }} + style={{ + position: "absolute", + zIndex: 1, + left: "10px", + backgroundColor: "#456BD9", + border: "0.1875em solid #0F1C3F", + borderRadius: "50%", + height: "60px", + width: "60px", + cursor: "pointer", + }} + > + +
+ + {props.largeText} + + + ) : ( + <> + + {props.largeText} + + + )} +
+ + {props.minText !== undefined ? ( + + ) : ( + <> + )} + + ); +}; diff --git a/ui/src/core/ui/link/link.tsx b/ui/src/core/ui/link/link.tsx new file mode 100644 index 0000000..05e3bf3 --- /dev/null +++ b/ui/src/core/ui/link/link.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import { Typography } from "antd"; +import { useNavigate } from "react-router-dom"; + +const { Link } = Typography; + +export interface ILinkTypography { + path: string; + text: string; + style?: React.CSSProperties; +} + +export const LinkTypography: React.FunctionComponent = ( + props: ILinkTypography +) => { + const navigate = useNavigate(); + + return ( + { + navigate(props.path); + }} + > + {props.text} + + ); +}; diff --git a/ui/src/core/ui/list/list.tsx b/ui/src/core/ui/list/list.tsx new file mode 100644 index 0000000..7a165fb --- /dev/null +++ b/ui/src/core/ui/list/list.tsx @@ -0,0 +1,107 @@ +import { Row } from "antd"; +import { ReactComponent as AddIcon } from "../../assets/icons/add.svg"; +import { ReactComponent as DeleteIcon } from "../../assets/icons/delete.svg"; + +import { observer } from "mobx-react-lite"; +import { v4 } from "uuid"; +import { ILinkTypography, LinkTypography } from "../link/link"; + +export type CallBackFunction = (el: ListElement, index: number) => void; + +export interface ListElement { + id?: string; + text: string; + color?: string; +} + +export enum Icon { + add, + delete, +} +export interface IPropsList { + values: ListElement[]; + headers?: string; + link?: ILinkTypography; + onClick?: CallBackFunction; + icon: Icon; +} + +export const List: React.FunctionComponent = observer((props) => { + props.values.map((el) => { + if (el.id === undefined) { + el.id = v4(); + return el; + } + return el; + }); + return ( +
+ {props.headers !== undefined ? <>{props.headers} : <>} + {props.link !== undefined ? ( +
+ +
+ ) : ( + <> + )} + {props.values.map((el, index) => { + return ( + +
+ +
+ {el.text} +
+
+ {props.icon === Icon.add ? ( + <> + { + if (props.onClick !== undefined) { + props.onClick(el, index); + } + }} + /> + + ) : ( + { + if (props.onClick !== undefined) { + props.onClick(el, index); + } + }} + /> + )} + + ); + })} +
+ ); +}); diff --git a/ui/src/core/ui/loader/loader.css b/ui/src/core/ui/loader/loader.css new file mode 100644 index 0000000..6239784 --- /dev/null +++ b/ui/src/core/ui/loader/loader.css @@ -0,0 +1,31 @@ +.loader { + position: relative; + width: 64px; + height: 64px; + background-color: rgba(0, 0, 0, 0.5); + transform: rotate(45deg); + overflow: hidden; +} +.loader:after{ + content: ''; + position: absolute; + inset: 8px; + margin: auto; + background: #222b32; +} +.loader:before{ + content: ''; + position: absolute; + inset: -15px; + margin: auto; + background: #de3500; + animation: diamondLoader 2s linear infinite; +} +@keyframes diamondLoader { + 0% ,10% { + transform: translate(-64px , -64px) rotate(-45deg) + } + 90% , 100% { + transform: translate(0px , 0px) rotate(-45deg) + } +} \ No newline at end of file diff --git a/ui/src/core/ui/loader/loader.tsx b/ui/src/core/ui/loader/loader.tsx new file mode 100644 index 0000000..79e0f01 --- /dev/null +++ b/ui/src/core/ui/loader/loader.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import "./loader.css"; + +export const Loader: React.FunctionComponent = () => { + return ( +
+
+
+ ); +}; diff --git a/ui/src/core/ui/pages/load_page.tsx b/ui/src/core/ui/pages/load_page.tsx new file mode 100644 index 0000000..5903ad6 --- /dev/null +++ b/ui/src/core/ui/pages/load_page.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { Header, IHeader } from "../header/header"; +import { Loader } from "../loader/loader"; +import { ReactComponent as ErrorIcon } from "../../assets/icons/error.svg"; +import { Typography } from "antd"; +import { observer } from "mobx-react-lite"; +const { Title } = Typography; + +interface ILoadPage extends IHeader { + isLoading: boolean; + isError: boolean; + children?: JSX.Element | JSX.Element[]; +} + +export const LoadPage: React.FunctionComponent = observer( + (props: ILoadPage) => { + return ( + <> +
+ {props.isError ? ( + <> + + + not expected error + + + ) : ( + <> + {props.isLoading ? ( +
+ +
+ ) : ( + <>{props.children} + )} + + )} + + ); + } +); diff --git a/ui/src/features/all_projects/data/project_repository.ts b/ui/src/features/all_projects/data/project_repository.ts new file mode 100644 index 0000000..19641a9 --- /dev/null +++ b/ui/src/features/all_projects/data/project_repository.ts @@ -0,0 +1,27 @@ +import { redirect } from "react-router-dom"; +import { ActivePipeline } from "../../../core/model/active_pipiline"; +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { PipelineInstanceScreenPath } from "../../pipeline_instance_main_screen/pipeline_instance_screen"; +import { IProjectModel } from "../model/project_model"; + +export class ProjectRepository extends HttpRepository { + async getAllProject() { + return this.jsonRequest(HttpMethod.GET, "/project"); + } + + async getActivePipeline() { + return this.jsonRequest(HttpMethod.GET, "/realtime"); + } + loader = async () => { + const result = await this.getActivePipeline(); + + // if (result.isSuccess() && result.value.projectUUID !== null) { + // return redirect(PipelineInstanceScreenPath + result.value.projectUUID); + // } + + return null; + }; +} diff --git a/ui/src/features/all_projects/model/project_model.ts b/ui/src/features/all_projects/model/project_model.ts new file mode 100644 index 0000000..e74166b --- /dev/null +++ b/ui/src/features/all_projects/model/project_model.ts @@ -0,0 +1,8 @@ +import { PipelineModel } from "../../create_project/create_project_repository"; + +export interface IProjectModel { + _id?: string; + pipelines: [PipelineModel]; + rootDir: string; + description: string; +} diff --git a/ui/src/features/all_projects/presentation/all_projects_screen.tsx b/ui/src/features/all_projects/presentation/all_projects_screen.tsx new file mode 100644 index 0000000..6248f6b --- /dev/null +++ b/ui/src/features/all_projects/presentation/all_projects_screen.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import { AllProjectStore } from "./all_projects_store"; +import { ProjectRepository } from "../data/project_repository"; +import { LoadPage } from "../../../core/ui/pages/load_page"; +import { observer } from "mobx-react-lite"; +import { SelectProjectScreenPath } from "../../select_project/presentation/select_project"; + +export const AllProjectScreenPath = "/"; + +export const AllProjectScreen: React.FunctionComponent = observer(() => { + const [allProjectStore] = React.useState( + () => new AllProjectStore(new ProjectRepository()) + ); + + return ( + <> + + {allProjectStore.projectsModels?.map((el) => { + return
{el.description}
; + })} +
+ } + /> + + ); +}); diff --git a/ui/src/features/all_projects/presentation/all_projects_store.ts b/ui/src/features/all_projects/presentation/all_projects_store.ts new file mode 100644 index 0000000..7851c79 --- /dev/null +++ b/ui/src/features/all_projects/presentation/all_projects_store.ts @@ -0,0 +1,24 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { ProjectRepository } from "../data/project_repository"; +import { IProjectModel } from "../model/project_model"; +import { BaseStore } from "../../../core/store/base_store"; + +export class AllProjectStore extends BaseStore { + projectsModels?: IProjectModel[]; + repository: ProjectRepository; + redirect = false; + constructor(repository: ProjectRepository) { + super(); + this.repository = repository; + makeAutoObservable(this); + } + + async getProjects() { + const result = await this.loadingHelper(this.repository.getAllProject()); + if (result.isSuccess()) { + this.projectsModels = result.value; + } + } + + +} diff --git a/ui/src/features/create_pipeline/data/create_pipeline_repository.ts b/ui/src/features/create_pipeline/data/create_pipeline_repository.ts new file mode 100644 index 0000000..c12a8f1 --- /dev/null +++ b/ui/src/features/create_pipeline/data/create_pipeline_repository.ts @@ -0,0 +1,26 @@ +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { ITriggerModel } from "../../../core/model/trigger_model"; +import { Result } from "../../../core/helper/result"; +import { IProcess } from "../../create_process/model/process_model"; +import { PipelineModelDataBase } from "../model/pipeline_model"; + +export class CreatePipelineRepository extends HttpRepository { + async savePipeline( + model: PipelineModelDataBase + ): Promise> { + return await this.jsonRequest(HttpMethod.POST, `/pipeline`, model); + } + async getTriggers(page = 1): Promise> { + return await this.jsonRequest(HttpMethod.GET, `/trigger?${page}`); + } + + async getProcessed(page = 1): Promise> { + return await this.jsonRequest( + HttpMethod.GET, + `/process?${page}` + ); + } +} diff --git a/ui/src/features/create_pipeline/model/pipeline_model.ts b/ui/src/features/create_pipeline/model/pipeline_model.ts new file mode 100644 index 0000000..74352a1 --- /dev/null +++ b/ui/src/features/create_pipeline/model/pipeline_model.ts @@ -0,0 +1,8 @@ +export interface IColor { + color: string; +} + +export interface PipelineModelDataBase { + process: string; + trigger: string; +} \ No newline at end of file diff --git a/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx b/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx new file mode 100644 index 0000000..2d95a95 --- /dev/null +++ b/ui/src/features/create_pipeline/presentation/create_pipeline_screen.tsx @@ -0,0 +1,60 @@ +import * as React from "react"; +import { Row, Button } from "antd"; +import { LoadPage } from "../../../core/ui/pages/load_page"; +import { createPipelineStore } from "./create_pipeline_store"; +import { observer } from "mobx-react-lite"; +import { Icon, List } from "../../../core/ui/list/list"; +import { CreateTriggerScreenPath } from "../../create_trigger/presentation/create_trigger_screen"; +import { CreateProcessScreenPath } from "../../create_process/presentation/create_process_screen"; + +export const CreatePipelineScreenPath = "/create_pipeline"; + +export const CreatePipelineScreen: React.FunctionComponent = observer(() => { + return ( + <> + + + { + return { text: el.description, id: el._id }; + })} + onClick={(e) => createPipelineStore.addProcess(e.text, e.id!)} + icon={Icon.add} + /> +
+ + { + createPipelineStore.filterPipelineViewModel(index); + }} + /> +
+ + { + return { text: el.description, id: el._id }; + })} + onClick={(e) => createPipelineStore.addTrigger(e.text, e.id!)} + icon={Icon.add} + /> +
+ + } + /> + + ); +}); diff --git a/ui/src/features/create_pipeline/presentation/create_pipeline_store.ts b/ui/src/features/create_pipeline/presentation/create_pipeline_store.ts new file mode 100644 index 0000000..73ec514 --- /dev/null +++ b/ui/src/features/create_pipeline/presentation/create_pipeline_store.ts @@ -0,0 +1,128 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { CreatePipelineRepository } from "../data/create_pipeline_repository"; +import { ITriggerModel } from "../../../core/model/trigger_model"; +import { IProcess } from "../../create_process/model/process_model"; +import { message } from "antd"; +import { BaseStore } from "../../../core/store/base_store"; + +enum Type { + PROCESS, + TRIGGER, +} +export interface UnionView { + text: string; + color: string; + type: Type; + uuid?: string; +} + +export class CreatePipelineStore extends BaseStore { + repository: CreatePipelineRepository; + triggersModels: ITriggerModel[] = []; + processModels: IProcess[] = []; + pipelineViewModels: UnionView[] = []; + + constructor(repository: CreatePipelineRepository) { + super(); + this.repository = repository; + makeAutoObservable(this); + this.init(); + } + private init() { + this.loadTriggers(); + this.loadProcess(); + } + + filterPipelineViewModel(index: number): void { + this.pipelineViewModels = this.pipelineViewModels.filter( + (_el, i) => i !== index + ); + } + addTrigger(e: string, id: string): void { + const lastElement = this.pipelineViewModels.lastElement(); + if (this.pipelineViewModels.length === 2) { + return; + } + if (lastElement !== undefined) { + if (lastElement.type !== Type.PROCESS) { + message.error("Need process"); + + return; + } + } + this.pipelineViewModels.push({ + uuid: id, + text: e, + color: "blanchedalmond", + type: Type.TRIGGER, + }); + } + addProcess(e: string, id: string): void { + const lastElement = this.pipelineViewModels.lastElement(); + if (this.pipelineViewModels.length === 2) { + return; + } + if (lastElement !== undefined) { + if (lastElement.type !== Type.TRIGGER) { + message.error("Need trigger"); + return; + } + } + + this.pipelineViewModels.push({ + uuid: id, + text: e, + color: "activeborder", + type: Type.PROCESS, + }); + } + + async createPipeline(): Promise { + if (this.pipelineViewModels.isEmpty()) { + message.error("not found pipelines process"); + return; + } + const triggerId = this.pipelineViewModels.find( + (el) => el.type === Type.TRIGGER + )!.uuid as string; + const processId = this.pipelineViewModels.find( + (el) => el.type === Type.PROCESS + )!.uuid as string; + + this.repository.savePipeline({ + process: processId, + trigger: triggerId, + }); + } + + async loadProcess() { + this.isLoading = true; + const result = await this.repository.getProcessed(); + result.fold( + (s) => { + this.processModels = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } + + async loadTriggers() { + this.isLoading = true; + const result = await this.repository.getTriggers(1); + result.fold( + (s) => { + this.triggersModels = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } +} +export const createPipelineStore = new CreatePipelineStore( + new CreatePipelineRepository() +); diff --git a/ui/src/features/create_process/data/process_repostiory.ts b/ui/src/features/create_process/data/process_repostiory.ts new file mode 100644 index 0000000..fa4e44b --- /dev/null +++ b/ui/src/features/create_process/data/process_repostiory.ts @@ -0,0 +1,11 @@ +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { IProcess } from "../model/process_model"; + +export class ProcessRepository extends HttpRepository { + async save(model: IProcess): Promise { + await this.jsonRequest(HttpMethod.POST, "/process", model); + } +} diff --git a/ui/src/features/create_process/model/process_model.ts b/ui/src/features/create_process/model/process_model.ts new file mode 100644 index 0000000..ac128f4 --- /dev/null +++ b/ui/src/features/create_process/model/process_model.ts @@ -0,0 +1,29 @@ +import { DatabaseModel } from "../../../core/model/database_model"; + +export interface IProcess extends DatabaseModel { + description: string; + type: EXEC_TYPE | string; + command: string; + isGenerating: boolean; + isLocaleCode: boolean; + issueType: IssueType | string; + timeout?: number; + commit?: string | undefined; +} + +export enum EXEC_TYPE { + SPAWN = "SPAWN", + EXEC = "EXEC", +} +export enum IssueType { + WARNING = "WARNING", + ERROR = "ERROR", +} +export const processModelMock: IProcess = { + description: "", + type: EXEC_TYPE.SPAWN, + command: "", + isGenerating: true, + isLocaleCode: true, + issueType: IssueType.WARNING, +}; diff --git a/ui/src/features/create_process/presentation/create_process_screen.tsx b/ui/src/features/create_process/presentation/create_process_screen.tsx new file mode 100644 index 0000000..27159ce --- /dev/null +++ b/ui/src/features/create_process/presentation/create_process_screen.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; +import { processStore } from "./logic/process_store"; +import { observer } from "mobx-react-lite"; +import { + SubmitButton, + Input, + ResetButton, + Form, + Radio, + Switch, +} from "formik-antd"; +import { Formik } from "formik"; +import { Row, Col } from "antd"; +import { EXEC_TYPE, IssueType, processModelMock } from "../model/process_model"; +export const CreateProcessScreenPath = '/create/process' +export const CreateProcessScreen = observer(() => { + return ( +
+
+ { + await processStore.saveResult(values); + actions.setSubmitting(false); + actions.resetForm(); + }} + validate={(values) => { + if (!values.command) { + return { command: "required" }; + } + if (!values.description) { + return { description: "required" }; + } + return {}; + }} + render={() => ( +
+
+ + + + + + + + + + + + + + + + Reset + Submit + + +
+
+ )} + /> +
+
+ ); +}); diff --git a/ui/src/features/create_process/presentation/logic/process_store.ts b/ui/src/features/create_process/presentation/logic/process_store.ts new file mode 100644 index 0000000..127cb56 --- /dev/null +++ b/ui/src/features/create_process/presentation/logic/process_store.ts @@ -0,0 +1,16 @@ +import { makeAutoObservable } from "mobx"; +import { ProcessRepository } from "../../data/process_repostiory"; +import { IProcess } from "../../model/process_model"; + +class ProcessStore { + repository: ProcessRepository; + constructor(repository: ProcessRepository) { + this.repository = repository; + makeAutoObservable(this); + } + async saveResult(model:IProcess) { + await this.repository.save(model) + } +} + +export const processStore = new ProcessStore(new ProcessRepository()); diff --git a/ui/src/features/create_project/create_project_repository.ts b/ui/src/features/create_project/create_project_repository.ts new file mode 100644 index 0000000..325ccfe --- /dev/null +++ b/ui/src/features/create_project/create_project_repository.ts @@ -0,0 +1,25 @@ +import { Result } from "../../core/helper/result"; +import { DatabaseModel } from "../../core/model/database_model"; +import { ITriggerModel } from "../../core/model/trigger_model"; +import { + HttpMethod, + HttpRepository, +} from "../../core/repository/http_repository"; +import { IProcess } from "../create_process/model/process_model"; +import { ICreateProjectViewModel } from "./project_model"; + +export interface PipelineModel extends DatabaseModel { + process: IProcess; + trigger: ITriggerModel; +} + +export class CreateProjectRepository extends HttpRepository { + async getAllPipelines(page = 1): Promise> { + return await this.jsonRequest(HttpMethod.GET, "/pipeline"); + } + async saveProject( + model: ICreateProjectViewModel + ): Promise> { + return await this.jsonRequest(HttpMethod.POST, "/project", model); + } +} diff --git a/ui/src/features/create_project/create_project_screen.tsx b/ui/src/features/create_project/create_project_screen.tsx new file mode 100644 index 0000000..f12cc10 --- /dev/null +++ b/ui/src/features/create_project/create_project_screen.tsx @@ -0,0 +1,90 @@ +import * as React from "react"; +import { LoadPage } from "../../core/ui/pages/load_page"; +import { createProjectStore } from "./create_project_store"; +import { observer } from "mobx-react-lite"; +import { Col, Row, Input, Button } from "antd"; +import { ReactComponent as AddIcon } from "../../core/assets/icons/add.svg"; +import { CreatePipelineScreenPath } from "../create_pipeline/presentation/create_pipeline_screen"; + +export const CreateProjectScreenPath = "/create_project"; + +export const CreateProjectScreen: React.FunctionComponent = observer(() => { + return ( + <> + + + <>Pipelines + {createProjectStore.pipelineModels?.map((el) => { + return ( +
+
{el.process.description}
+
{el.trigger.description}
+ { + createProjectStore.addPipeline(el); + }} + /> +
+ ); + })} + + + + + + createProjectStore.setDescriptionToNewProject( + e.target.value + ) + } + placeholder="project description" + /> + + + + + {createProjectStore.newProjectViews.map((el, index) => { + return ( +
+
{el.process.description}
+
{el.trigger.description}
+
{index + 1}
+
+ ); + })} + +
+ } + /> + + ); +}); + \ No newline at end of file diff --git a/ui/src/features/create_project/create_project_store.ts b/ui/src/features/create_project/create_project_store.ts new file mode 100644 index 0000000..73f634a --- /dev/null +++ b/ui/src/features/create_project/create_project_store.ts @@ -0,0 +1,77 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { + CreateProjectRepository, + PipelineModel, +} from "./create_project_repository"; +import { message } from "antd"; +import { BaseStore } from "../../core/store/base_store"; + +class CreateProjectStore extends BaseStore { + repository: CreateProjectRepository; + + pipelineModels?: PipelineModel[]; + newProjectDescription: string = ""; + newProjectViews: PipelineModel[] = []; + + constructor(repository: CreateProjectRepository) { + super(); + this.repository = repository; + makeAutoObservable(this); + this.loadPipelines(); + } + + async addPipeline(model: PipelineModel) { + this.newProjectViews.push(model); + } + + async loadPipelines() { + this.isLoading = true; + const result = await this.repository.getAllPipelines(); + result.fold( + (s) => { + this.pipelineModels = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } + + setDescriptionToNewProject(value: string): void { + this.newProjectDescription = value; + } + + async saveProject(): Promise { + if (this.newProjectDescription.isEmpty()) { + message.error("project description is not empty"); + return; + } + if (this.newProjectViews.isEmpty()) { + message.error("project view is not empty"); + return; + } + this.isLoading = true; + const result = await this.repository.saveProject({ + description: this.newProjectDescription, + pipelines: this.newProjectViews.map((el) => el._id ?? ""), + }); + + this.newProjectDescription = ""; + this.newProjectViews = []; + this.isLoading = false; + + result.fold( + (_s) => { + message.success("save"); + }, + (_e) => { + this.isError = true; + } + ); + } +} + +export const createProjectStore = new CreateProjectStore( + new CreateProjectRepository() +); diff --git a/ui/src/features/create_project/project_model.ts b/ui/src/features/create_project/project_model.ts new file mode 100644 index 0000000..a124f49 --- /dev/null +++ b/ui/src/features/create_project/project_model.ts @@ -0,0 +1,5 @@ +export interface ICreateProjectViewModel { + pipelines: string[]; + + description: string; +} diff --git a/ui/src/features/create_project_instance/create_project_instance.tsx b/ui/src/features/create_project_instance/create_project_instance.tsx new file mode 100644 index 0000000..cc0463b --- /dev/null +++ b/ui/src/features/create_project_instance/create_project_instance.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import { CreateProjectInstanceStore } from "./create_project_instance_store"; +import { CreateProjectInstanceRepository } from "./create_project_instance_repository"; +import { observer } from "mobx-react-lite"; +import { Upload, Button } from "antd"; +import { useParams } from "react-router-dom"; + +export const CreateProjectInstancePath = "/create/project/instance/"; + +export const CreateProjectInstanceScreen = observer(() => { + const [createProjectInstanceStore] = React.useState( + () => new CreateProjectInstanceStore(new CreateProjectInstanceRepository()) + ); + const id = useParams().id; + createProjectInstanceStore.getProjectById(id as string) + return ( + <> + { + console.log(e); + }} + > + + + + ); +}); diff --git a/ui/src/features/create_project_instance/create_project_instance_repository.ts b/ui/src/features/create_project_instance/create_project_instance_repository.ts new file mode 100644 index 0000000..6b6d1b9 --- /dev/null +++ b/ui/src/features/create_project_instance/create_project_instance_repository.ts @@ -0,0 +1,10 @@ +import { + HttpMethod, + HttpRepository, +} from "../../core/repository/http_repository"; + +export class CreateProjectInstanceRepository extends HttpRepository { + async getProjectInstance(id: string) { + return await this.jsonRequest(HttpMethod.GET, ""); + } +} diff --git a/ui/src/features/create_project_instance/create_project_instance_store.ts b/ui/src/features/create_project_instance/create_project_instance_store.ts new file mode 100644 index 0000000..be7a346 --- /dev/null +++ b/ui/src/features/create_project_instance/create_project_instance_store.ts @@ -0,0 +1,18 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { BaseStore } from "../../core/store/base_store"; +import { CreateProjectInstanceRepository } from "./create_project_instance_repository"; + +export class CreateProjectInstanceStore extends BaseStore { + constructor(repository: CreateProjectInstanceRepository) { + super(); + this.repository = repository; + makeAutoObservable(this); + } + repository: CreateProjectInstanceRepository; + async getProjectById(id: string) { + const result = await this.loadingHelper(this.repository.getProjectInstance(id)) + if(result.isSuccess()){ + + } + } +} diff --git a/ui/src/features/create_trigger/data/trigger_repository.ts b/ui/src/features/create_trigger/data/trigger_repository.ts new file mode 100644 index 0000000..3127310 --- /dev/null +++ b/ui/src/features/create_trigger/data/trigger_repository.ts @@ -0,0 +1,11 @@ +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { ITriggerModel } from "../../../core/model/trigger_model"; + +export class TriggerRepository extends HttpRepository { + public async save(model: ITriggerModel) { + return await this.jsonRequest(HttpMethod.POST, "/trigger", model); + } +} diff --git a/ui/src/features/create_trigger/model/trigger_form_view_model.ts b/ui/src/features/create_trigger/model/trigger_form_view_model.ts new file mode 100644 index 0000000..9ffe677 --- /dev/null +++ b/ui/src/features/create_trigger/model/trigger_form_view_model.ts @@ -0,0 +1,10 @@ +import { TriggerType } from "../../../core/model/trigger_model"; + +export interface ITriggerViewValueModel { + value: string; +} + +export interface TriggerViewModel extends ITriggerViewValueModel { + id: string; + type: TriggerType; +} diff --git a/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx b/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx new file mode 100644 index 0000000..14e7946 --- /dev/null +++ b/ui/src/features/create_trigger/presentation/components/code_trigger_form.tsx @@ -0,0 +1,37 @@ +import * as React from "react"; +import Editor from "@monaco-editor/react"; +import { Button } from "antd"; +import { observer } from "mobx-react-lite"; +import { triggerStore } from "../trigger_store"; + +export const CodeTriggerForm: React.FunctionComponent = observer(() => { + return ( + <> +
+ + { + triggerStore.writeNewTrigger(v); + }} + onValidate={(_m) => {}} + /> + +
+
+ + + + + + ); +}); diff --git a/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx b/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx new file mode 100644 index 0000000..a09b70f --- /dev/null +++ b/ui/src/features/create_trigger/presentation/components/file_trigger_form.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import { Formik } from "formik"; +import { SubmitButton, Input, ResetButton, Form, FormItem } from "formik-antd"; +import { Row, Col } from "antd"; +import { observer } from "mobx-react-lite"; +import { triggerStore } from "../trigger_store"; +import { TriggerType } from "../../../../core/model/trigger_model"; +import { validateRequired } from "../../../../core/helper/validate"; + +export const FileTriggerForm: React.FunctionComponent = observer(() => { + return ( + <> +
+ { + triggerStore.pushTrigger(values.value, TriggerType.FILE); + actions.setSubmitting(false); + actions.resetForm(); + }} + validate={(values) => { + if (values.value.length === 0) { + return false; + } + return {}; + }} + render={() => ( +
+
+ + + + + + Reset + Submit + + +
+
+ )} + /> +
+ + ); +}); diff --git a/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx b/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx new file mode 100644 index 0000000..73524e2 --- /dev/null +++ b/ui/src/features/create_trigger/presentation/create_trigger_screen.tsx @@ -0,0 +1,80 @@ +import * as React from "react"; +import { Button, Col, Row, Switch, Typography, Input } from "antd"; +import { CodeTriggerForm } from "./components/code_trigger_form"; +import { observer } from "mobx-react-lite"; +import { triggerStore } from "./trigger_store"; +import { FileTriggerForm } from "./components/file_trigger_form"; +import { ReactComponent as DeleteIcon } from "../../../core/assets/icons/delete.svg"; +import { Loader } from "../../../core/ui/loader/loader"; + +const { Title } = Typography; + +const Header = observer(() => { + return ( + +
+ triggerStore.setTriggerType()} + /> +
+ + Trigger editor: {triggerStore.getTriggerDescription()} + +
+ +
+ ); +}); + +const Bottom = observer(() => { + return ( + + {triggerStore.triggers.map((el) => { + return ( + + {el.value} + triggerStore.deleteItem(el.id)} /> + + ); + })} + + ); +}); +export const CreateTriggerScreenPath = '/create/trigger' +export const TriggerScreen: React.FunctionComponent = observer(() => { + return ( + <> +
+ {!triggerStore.isLoading ? ( + <> +
, + triggerStore.changeTriggerDescription} + /> + {triggerStore.getTriggerType() ? ( + <> + + + ) : ( + <> + + + )} + + + ) : ( + <> + + + + )} +
+ + ); +}); diff --git a/ui/src/features/create_trigger/presentation/trigger_store.ts b/ui/src/features/create_trigger/presentation/trigger_store.ts new file mode 100644 index 0000000..efbfde7 --- /dev/null +++ b/ui/src/features/create_trigger/presentation/trigger_store.ts @@ -0,0 +1,87 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { v4 as uuidv4 } from "uuid"; +import { TriggerType } from "../../../core/model/trigger_model"; +import { TriggerRepository } from "../data/trigger_repository"; +import { TriggerViewModel } from "../model/trigger_form_view_model"; +import { BaseStore } from "../../../core/store/base_store"; + +class TriggerStore extends BaseStore { + constructor(repository: TriggerRepository) { + super(); + this.triggerType = TriggerType.FILE; + this.repository = repository; + makeAutoObservable(this); + } + + triggerDescription: string = ""; + triggerType: TriggerType; + codeTriggerValue = ""; + triggers: TriggerViewModel[] = []; + repository: TriggerRepository; + + changeTriggerDescription(value: string): void { + this.triggerDescription = value; + } + + deleteItem(id: string): void { + this.triggers = this.triggers.filter((el) => el.id !== id); + } + + getTriggerType = (): boolean => { + return this.triggerType === TriggerType.FILE; + }; + setTriggerType = (): void => { + this.triggers = []; + if (this.triggerType === TriggerType.FILE) { + this.triggerType = TriggerType.PROCESS; + return; + } + this.triggerType = TriggerType.FILE; + }; + getTriggerDescription = (): string => { + return this.triggerType === TriggerType.FILE + ? TriggerType.FILE + : TriggerType.PROCESS; + }; + pushTrigger = (value: string, type: TriggerType): void => { + this.triggers.push({ + value: value, + id: uuidv4(), + type: type, + }); + }; + + writeNewTrigger(v: string | undefined): void { + if (v === undefined) { + throw new Error("Editor Value is undefined"); + } + this.codeTriggerValue = v; + } + clearTriggerCode(): void { + this.codeTriggerValue = ""; + } + saveCode(): void { + if (this.codeTriggerValue !== "") { + this.triggers.push({ + id: uuidv4(), + value: this.codeTriggerValue, + type: TriggerType.PROCESS, + }); + + this.codeTriggerValue = ""; + } + } + async saveResult(): Promise { + this.isLoading = true; + await this.repository.save({ + type: this.getTriggerDescription(), + description: this.triggerDescription, + value: this.triggers.map((el) => { + return el.value; + }), + }); + this.isLoading = false; + } +} + +export const triggerStore = new TriggerStore(new TriggerRepository()); diff --git a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_repository.ts b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_repository.ts new file mode 100644 index 0000000..c43eff2 --- /dev/null +++ b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_repository.ts @@ -0,0 +1 @@ +export class PipelineInstanceRepository {} diff --git a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx new file mode 100644 index 0000000..6efdf6d --- /dev/null +++ b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_screen.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { LoadPage } from "../../core/ui/pages/load_page"; +import { PipelineInstanceStore } from "./pipeline_instance_store"; + +export const PipelineInstanceScreenPath = "/pipeline_instance/"; +export const PipelineInstanceScreen: React.FunctionComponent = () => { + const [pipelineInstanceStore] = React.useState( + () => new PipelineInstanceStore() + ); + + return ( +
} + /> + ); +}; diff --git a/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts new file mode 100644 index 0000000..e51c874 --- /dev/null +++ b/ui/src/features/pipeline_instance_main_screen/pipeline_instance_store.ts @@ -0,0 +1,9 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { BaseStore } from "../../core/store/base_store"; + +export class PipelineInstanceStore extends BaseStore { + constructor() { + super(); + makeAutoObservable(this); + } +} diff --git a/ui/src/features/scene_manager/components/static_asset_item_view.tsx b/ui/src/features/scene_manager/components/static_asset_item_view.tsx new file mode 100644 index 0000000..a60272b --- /dev/null +++ b/ui/src/features/scene_manager/components/static_asset_item_view.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; +import { StaticAssetItemModel } from "../scene_manager_store"; + +export interface IStaticAssetModelViewProps { + model: StaticAssetItemModel; +} + +export function StaticAssetModelView(props: IStaticAssetModelViewProps) { + return ( +
+ {props.model.name} +
+ ); +} diff --git a/ui/src/features/scene_manager/scene_manager.tsx b/ui/src/features/scene_manager/scene_manager.tsx new file mode 100644 index 0000000..d3e03b7 --- /dev/null +++ b/ui/src/features/scene_manager/scene_manager.tsx @@ -0,0 +1,55 @@ +import * as React from "react"; +import { SceneMangerStore, StaticAssetItemModel } from "./scene_manager_store"; +import { observer } from "mobx-react-lite"; +import { StaticAssetModelView } from "./components/static_asset_item_view"; + +// const useKeyLister = (fn: Function) => { +// const pressed = new Map(); + +// const registerKeyPress = React.useCallback( +// (event: KeyboardEvent, codes: string[], callBack: Function) => { +// if (codes.hasIncludeElement(event.code)) { +// pressed.addValueOrMakeCallback(event.code, event.type, (e) => { +// if (Array.from(pressed.values()).equals(["keydown", "keydown"], false)) { +// callBack(); +// } +// }); +// } +// }, +// [pressed] +// ); + +// React.useEffect(() => { +// window.addEventListener("keyup", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => fn)); +// window.addEventListener("keydown", (e) => registerKeyPress(e, ["KeyQ", "KeyW"], () => {})); +// }, [fn, registerKeyPress]); + +// return []; +// }; + +export const SceneManger = observer(() => { + const canvasRef = React.useRef(null); + + const [sceneMangerStore] = React.useState(() => new SceneMangerStore()); + + React.useEffect(() => { + sceneMangerStore.loadGl(canvasRef.current!); + return () => { + sceneMangerStore.dispose(); + }; + }); + + return ( +
+
+ {sceneMangerStore.sceneItems.map((el) => { + if (el instanceof StaticAssetItemModel) { + return StaticAssetModelView({ model: el }); + } + return <>; + })} +
+ +
+ ); +}); diff --git a/ui/src/features/scene_manager/scene_manager_store.ts b/ui/src/features/scene_manager/scene_manager_store.ts new file mode 100644 index 0000000..96ae25e --- /dev/null +++ b/ui/src/features/scene_manager/scene_manager_store.ts @@ -0,0 +1,70 @@ +/* eslint-disable array-callback-return */ +import { makeAutoObservable } from "mobx"; +import { CoreThereRepository } from "../../core/repository/core_there_repository"; +import { v4 as uuidv4 } from "uuid"; +import { Vector2 } from "three"; + +export class BaseSceneItemModel { + id: string; + constructor() { + this.id = uuidv4(); + } +} + +enum SceneModelsType { + ASSET, +} + +export class StaticAssetItemModel extends BaseSceneItemModel { + name: string; + type = SceneModelsType.ASSET; + constructor(name: string) { + super(); + this.name = name; + } +} + +export class SceneMangerStore { + coreThereRepository: null | CoreThereRepository = null; + sceneItems: BaseSceneItemModel[] = []; + + constructor() { + makeAutoObservable(this); + } + + loadGl(canvasRef: HTMLCanvasElement): void { + this.coreThereRepository = new CoreThereRepository(canvasRef as HTMLCanvasElement); + this.coreThereRepository.on(this.watcherThereObjects); + this.coreThereRepository.render(); + this.coreThereRepository.fitCameraToCenteredObject(this.coreThereRepository.getAllSceneNameModels()); + + this.sceneItems = this.coreThereRepository.getAllSceneModels(); + + window.addEventListener("click", this.handleMouseClick); + } + + watcherThereObjects(sceneItemModel: BaseSceneItemModel): void { + this.sceneItems.push(sceneItemModel); + } + + handleMouseClick = (event: MouseEvent) => { + const vector = new Vector2(); + console.log("===="); + console.log(event.pageX); + console.log(event.clientX); + console.log(event.x); + console.log(event.movementX); + console.log(event.screenX); + console.log("===="); + vector.x = (event.clientX / window.innerWidth) * 2 - 1; + vector.y = -(event.clientY / window.innerHeight) * 2 + 1; + + this.coreThereRepository?.setRayCastAndGetFirstObject(vector).map((el) => { + this.coreThereRepository?.switchObjectEmissive(el); + }); + }; + + dispose() { + window.removeEventListener("click", this.handleMouseClick); + } +} diff --git a/ui/src/features/select_project/data/select_project_repository.ts b/ui/src/features/select_project/data/select_project_repository.ts new file mode 100644 index 0000000..67ee41f --- /dev/null +++ b/ui/src/features/select_project/data/select_project_repository.ts @@ -0,0 +1,12 @@ +import { Result } from "../../../core/helper/result"; +import { + HttpMethod, + HttpRepository, +} from "../../../core/repository/http_repository"; +import { IProjectModel } from "../model/project_model"; + +export class SelectProjectRepository extends HttpRepository { + async getAllProjects(page = 1): Promise> { + return await this.jsonRequest(HttpMethod.GET, `/project?${page}`); + } +} diff --git a/ui/src/features/select_project/model/project_model.ts b/ui/src/features/select_project/model/project_model.ts new file mode 100644 index 0000000..6b98740 --- /dev/null +++ b/ui/src/features/select_project/model/project_model.ts @@ -0,0 +1,9 @@ +import { DatabaseModel } from "../../../core/model/database_model"; +import { IProcess } from "../../create_process/model/process_model"; + +export interface IProjectModel extends DatabaseModel { + pipelines: [IProcess]; + rootDir: string; + description: string; +} + \ No newline at end of file diff --git a/ui/src/features/select_project/presentation/select_project.tsx b/ui/src/features/select_project/presentation/select_project.tsx new file mode 100644 index 0000000..543820d --- /dev/null +++ b/ui/src/features/select_project/presentation/select_project.tsx @@ -0,0 +1,43 @@ +import * as React from "react"; +import { observer } from "mobx-react-lite"; +import { LoadPage } from "../../../core/ui/pages/load_page"; +import { CreateProjectScreenPath } from "../../create_project/create_project_screen"; +import { SelectProjectStore } from "./select_project_store"; +import { SelectProjectRepository } from "../data/select_project_repository"; +import { useNavigate } from "react-router-dom"; +import { CreateProjectInstancePath } from "../../create_project_instance/create_project_instance"; +import { Button } from "antd"; +export const SelectProjectScreenPath = "/select_project"; + +export const SelectProjectScreen: React.FunctionComponent = observer(() => { + const [selectProjectStore] = React.useState( + () => new SelectProjectStore(new SelectProjectRepository()) + ); + const navigate = useNavigate(); + + return ( + <> + { + return ( + <> +
{el.description}
+
+ +
+ + ); + })} + /> + + ); +}); diff --git a/ui/src/features/select_project/presentation/select_project_store.ts b/ui/src/features/select_project/presentation/select_project_store.ts new file mode 100644 index 0000000..e950a7f --- /dev/null +++ b/ui/src/features/select_project/presentation/select_project_store.ts @@ -0,0 +1,33 @@ +import makeAutoObservable from "mobx-store-inheritance"; +import { SelectProjectRepository } from "../data/select_project_repository"; +import { IProjectModel } from "../model/project_model"; +import { BaseStore } from "../../../core/store/base_store"; + +export class SelectProjectStore extends BaseStore { + repository: SelectProjectRepository; + + page = 1; + projects: IProjectModel[] = []; + + constructor(repository: SelectProjectRepository) { + super() + this.repository = repository; + makeAutoObservable(this); + this.getPipelines(); + } + + async getPipelines(): Promise { + this.isLoading = true; + const result = await this.repository.getAllProjects(this.page); + result.fold( + (s) => { + this.projects = s; + }, + (_e) => { + this.isError = true; + } + ); + this.isLoading = false; + } +} + \ No newline at end of file diff --git a/ui/src/features/socket_lister/socket_lister.tsx b/ui/src/features/socket_lister/socket_lister.tsx new file mode 100644 index 0000000..abc7004 --- /dev/null +++ b/ui/src/features/socket_lister/socket_lister.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { ReactComponent as ReloadIcon } from "../../core/assets/icons/reload.svg"; +import { socketListerStore } from "./socket_lister_store"; +import { observer } from "mobx-react-lite"; + +export interface ISocketListerProps { + children?: JSX.Element; +} + +export const SocketLister = observer((props: ISocketListerProps) => { + return ( + <> + {socketListerStore.socketHasDisconnect ? ( + { + socketListerStore.reconnect(); + }} + style={{ + height: "70px", + backgroundColor: "silver", + width: "-webkit-fill-available", + padding: "10px", + cursor: "pointer", + }} + /> + ) : ( + <> + )} + + {props.children} + + ); +}); diff --git a/ui/src/features/socket_lister/socket_lister_store.ts b/ui/src/features/socket_lister/socket_lister_store.ts new file mode 100644 index 0000000..d744af9 --- /dev/null +++ b/ui/src/features/socket_lister/socket_lister_store.ts @@ -0,0 +1,20 @@ +import { makeAutoObservable } from "mobx"; +import { SocketRepository } from "../../core/repository/socket_repository"; + +class SocketListerStore { + repository: SocketRepository; + socketHasDisconnect = false; + + constructor(repository: SocketRepository) { + this.repository = repository; + makeAutoObservable(this); + repository.connect() + } + + async reconnect() { + await this.repository.connect() + this.socketHasDisconnect = false + } +} + +export const socketListerStore = new SocketListerStore(new SocketRepository()); diff --git a/ui/src/index.tsx b/ui/src/index.tsx new file mode 100644 index 0000000..571cfd8 --- /dev/null +++ b/ui/src/index.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; + +import "antd/dist/antd.min.css"; +import { RouterProvider } from "react-router-dom"; + +import { router } from "./core/routers/routers"; +import { SocketLister } from "./features/socket_lister/socket_lister"; +import { extensions } from "./core/extensions/extensions"; +import { SceneManger } from "./features/scene_manager/scene_manager"; + +extensions(); +const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); + +root.render( + <> + {/* + + */} + + +); diff --git a/ui/src/react-app-env.d.ts b/ui/src/react-app-env.d.ts new file mode 100644 index 0000000..6431bc5 --- /dev/null +++ b/ui/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..397b455 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "useDefineForClassFields": true + }, + "include": ["src"] +}