Staging implementation (#9)

The creation of a single repository is no longer relevant. Therefore, we
have to separate the front and back repo into two distinct repositories.
This PR allows to repatriate the work on dev branch
This commit is contained in:
hugolxt 2023-03-15 14:40:23 +01:00 committed by GitHub
parent 41a8f67da2
commit ead8f43b7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 2540 additions and 561 deletions

View File

@ -37,5 +37,8 @@
"rust-client.rustupPath": "${env:HOME}/elrondsdk/vendor-rust/bin/rustup",
"rust-client.rlsPath": "${env:HOME}/elrondsdk/vendor-rust/bin/rls",
"rust-client.disableRustup": true,
"rust-client.autoStartRls": false
"rust-client.autoStartRls": false,
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
}
}

1430
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"description": "tezosLink project",
"_moduleAliases": {
"@Api": "./dist/api",
"@Front": "./dist/front/*",
"@Front": "./dist/front",
"@Assets": "./dist/front/Assets/*",
"@Components": "./dist/front/Components/*",
"@Themes": "./dist/front/Themes/*",
@ -41,6 +41,9 @@
},
"homepage": "https://github.com/smart-chain-fr/tezosLink#readme",
"dependencies": {
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.11.12",
"@prisma/client": "^4.9.0",
"apexcharts": "^3.36.3",
"axios": "^1.3.3",
@ -52,6 +55,7 @@
"express": "^4.18.2",
"module-alias": "^2.2.2",
"next": "^13.1.5",
"node-schedule": "^2.1.1",
"prisma": "^4.9.0",
"prisma-query": "^2.0.0",
"react": "^18.2.0",
@ -71,6 +75,7 @@
"@types/cors": "^2.8.13",
"@types/express": "^4.17.16",
"@types/node": "^18.11.18",
"@types/node-schedule": "^2.1.0",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/uuid": "^9.0.0",

View File

@ -3,9 +3,8 @@ import { Controller, Get, Post } from "@ControllerPattern/index";
import { Service } from "typedi";
import { ProjectEntity } from "@Common/ressources";
import { IsNotEmpty, IsString, IsUUID, validateOrReject } from "class-validator";
import ProjectService from "@Services/project/ProjectService";
import ProjectService from "@Services/project/ProjectsService";
import ObjectHydrate from "@Common/helpers/ObjectHydrate";
import { processFindManyQuery } from "prisma-query";
import ApiController from "@Common/system/controller-pattern/ApiController";
class Params {
@ -24,8 +23,8 @@ export default class ProjectController extends ApiController {
@Get("/projects")
protected async get(req: Request, res: Response) {
const query = processFindManyQuery(req.query);
this.httpSuccess(res, await this.projectService.getByCriterias(query));
// const query = processFindManyQuery(req.query);
// this.httpSuccess(res, await this.projectService.getByCriterias(query));
}
@Get("/projects/:uuid")
@ -41,10 +40,10 @@ export default class ProjectController extends ApiController {
}
const project = await this.projectService.getByUUID(params);
if (!project) {
this.httpNotFoundRequest(res);
return;
}
// if (!project) {
// this.httpNotFoundRequest(res);
// return;
// }
this.httpSuccess(res, project);
}

View File

@ -0,0 +1,85 @@
import { Service } from "typedi";
import { validateOrReject, IsNotEmpty, IsOptional } from "class-validator";
import dotenv from "dotenv";
@Service()
export class BackendVariables {
@IsNotEmpty()
public readonly DATABASE_PORT!: string;
@IsNotEmpty()
public readonly DATABASE_HOSTNAME!: string;
@IsNotEmpty()
public readonly DATABASE_USER!: string;
@IsNotEmpty()
public readonly DATABASE_PASSWORD!: string;
@IsNotEmpty()
public readonly DATABASE_NAME!: string;
@IsNotEmpty()
public readonly API_HOSTNAME!: string;
@IsOptional()
public readonly API_LABEL!: string;
@IsNotEmpty()
public readonly API_PORT!: string;
@IsNotEmpty()
public readonly API_ROOT_URL!: string;
@IsOptional()
public readonly RPC_GATEWAY_LABEL!: string;
@IsNotEmpty()
public readonly RPC_GATEWAY_PORT!: string;
@IsNotEmpty()
public readonly RPC_GATEWAY_ROOT_URL!: string;
@IsNotEmpty()
public readonly ARCHIVE_NODES_URL!: string;
@IsNotEmpty()
public readonly ARCHIVE_NODES_PORT!: string;
@IsNotEmpty()
public readonly ROLLING_NODES_URL!: string;
@IsNotEmpty()
public readonly ROLLING_NODES_PORT!: string;
@IsNotEmpty()
public readonly TEZOS_NETWORK!: string;
public readonly NODE_ENV = process.env.NODE_ENV;
public constructor() {
dotenv.config();
this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
this.DATABASE_HOSTNAME = process.env["DATABASE_HOSTNAME"]!;
this.DATABASE_USER = process.env["DATABASE_USER"]!;
this.DATABASE_PASSWORD = process.env["DATABASE_PASSWORD"]!;
this.DATABASE_NAME = process.env["DATABASE_NAME"]!;
this.API_HOSTNAME = process.env["API_HOSTNAME"]!;
this.API_LABEL = process.env["API_LABEL"]!;
this.API_PORT = process.env["API_PORT"]!;
this.API_ROOT_URL = process.env["API_ROOT_URL"]!;
this.RPC_GATEWAY_LABEL = process.env["RPC_GATEWAY_LABEL"]!;
this.RPC_GATEWAY_PORT = process.env["RPC_GATEWAY_PORT"]!;
this.RPC_GATEWAY_ROOT_URL = process.env["RPC_GATEWAY_ROOT_URL"]!;
this.ARCHIVE_NODES_URL = process.env["ARCHIVE_NODES_URL"]!;
this.ARCHIVE_NODES_PORT = process.env["ARCHIVE_NODES_PORT"]!;
this.ROLLING_NODES_URL = process.env["ROLLING_NODES_URL"]!;
this.ROLLING_NODES_PORT = process.env["ROLLING_NODES_PORT"]!;
this.TEZOS_NETWORK = process.env["TEZOS_NETWORK"]!;
}
public async validate() {
await validateOrReject(this);
return this;
}
}

67
src/common/cron/Cron.ts Normal file
View File

@ -0,0 +1,67 @@
import { Service } from "typedi";
import IConfig from "./IConfig";
import schedule from "node-schedule";
import FunctionBinder, { IFunctionBinder } from "@Common/helpers/FunctionBinder";
type ICronTimer = {
second?: string | "*" | null;
minute?: string | "*" | null;
hour?: string | "*" | null;
dayMonth?: string | "*" | null;
month?: string | "*" | null;
dayWeek?: string | "*" | null;
};
@Service()
export default class Cron {
private readonly cronJobs: schedule.Job[] = [];
private static readonly runningJobs: boolean[] = [];
public async run(CronConfig: IConfig) {
this.cronJobs.splice(0);
this.cancel();
this.cronJobs.push(...CronConfig.jobs.filter((job) => job.enabled).map((job, index) => this.buildCronJob(job, index, CronConfig.binders)));
return this;
}
public cancel() {
this.cronJobs.forEach((job) => job.cancel());
return this;
}
public static createTimer(cronTimer: ICronTimer) {
return [cronTimer.second ?? "*", cronTimer.minute ?? "*", cronTimer.hour ?? "*", cronTimer.dayMonth ?? "*", cronTimer.month ?? "*", cronTimer.dayWeek ?? "*"].join(" ");
}
private buildCronJob(job: IConfig["jobs"][number], index: number, binders: IFunctionBinder[]) {
return schedule.scheduleJob(job.cronTime, () => Cron.scheduleJob(job, job.onTick, index, binders));
}
/**
* @description Prevent same jobs superposition
*/
private static async scheduleJob(jobConfig: IConfig["jobs"][number], cronCommand: () => Promise<any>, index: number, binders: IFunctionBinder[]) {
if (Cron.runningJobs[index]) return;
Cron.runningJobs[index] = true;
try {
console.info(`${Cron.getDate()} CronJob: ${jobConfig.cronTime} ${jobConfig.name}: started`);
if (binders.length) {
await FunctionBinder.bind(cronCommand, binders);
} else {
await cronCommand();
}
console.info(`${Cron.getDate()} CronJob: ${jobConfig.name}: end success`);
} catch (e) {
console.info(`${Cron.getDate()} CronJob: ${jobConfig.name}: end with error`);
console.error(e);
}
Cron.runningJobs[index] = false;
}
private static getDate() {
const d = new Date();
return `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()} ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`;
}
}

View File

@ -0,0 +1,12 @@
import { IFunctionBinder } from "@Common/helpers/FunctionBinder";
export default interface IConfig {
binders: IFunctionBinder[];
jobs: {
name: string;
description?: string;
cronTime: string | Date;
onTick: () => Promise<any>;
enabled?: boolean;
}[];
}

4
src/common/cron/index.ts Normal file
View File

@ -0,0 +1,4 @@
import IConfig from "@Common/cron/IConfig";
import Cron from "@Common/cron/Cron";
export type { IConfig };
export default Cron;

View File

@ -1,13 +1,14 @@
import { Service } from "typedi";
import DbProvider from "@Common/system/database";
import dotenv from "dotenv";
import { BackendVariables } from "@Common/config/variables/Variables";
dotenv.config();
@Service()
export default class Database {
protected readonly dbProvider: DbProvider;
constructor() {
constructor(private variables: BackendVariables) {
this.dbProvider = new DbProvider({
name: this.getDatabaseName(),
});
@ -21,7 +22,7 @@ export default class Database {
}
private getDatabaseName(): string {
const name = process.env["DATABASE_NAME"];
const name = this.variables.DATABASE_NAME;
if (!name) throw new Error("Database name is undefined!. Add name of db in the url.");
return name;
}

View File

@ -1,34 +1,34 @@
-- -- CreateTable
-- CREATE TABLE "app_projects" (
-- "id" SERIAL NOT NULL,
-- "title" VARCHAR(255) NOT NULL,
-- "uuid" VARCHAR(255) NOT NULL,
-- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- "updatedAt" TIMESTAMP(3) NOT NULL,
-- "network" VARCHAR(255) NOT NULL,
-- CreateTable
CREATE TABLE "app_projects" (
"id" SERIAL NOT NULL,
"title" VARCHAR(255) NOT NULL,
"uuid" VARCHAR(255) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"network" VARCHAR(255) NOT NULL,
-- CONSTRAINT "app_projects_pkey" PRIMARY KEY ("id")
-- );
CONSTRAINT "app_projects_pkey" PRIMARY KEY ("id")
);
-- -- CreateTable
-- CREATE TABLE "app_metrics" (
-- "id" SERIAL NOT NULL,
-- "path" VARCHAR(255) NOT NULL,
-- "uuid" VARCHAR(255) NOT NULL,
-- "remote_address" VARCHAR(255) NOT NULL,
-- "date_requested" TIMESTAMP(3) NOT NULL,
-- "projectId" INTEGER NOT NULL,
-- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- "updatedAt" TIMESTAMP(3) NOT NULL,
-- CreateTable
CREATE TABLE "app_metrics" (
"id" SERIAL NOT NULL,
"path" VARCHAR(255) NOT NULL,
"uuid" VARCHAR(255) NOT NULL,
"remote_address" VARCHAR(255) NOT NULL,
"date_requested" TIMESTAMP(3) NOT NULL,
"projectId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
-- CONSTRAINT "app_metrics_pkey" PRIMARY KEY ("id")
-- );
CONSTRAINT "app_metrics_pkey" PRIMARY KEY ("id")
);
-- -- CreateIndex
-- CREATE UNIQUE INDEX "app_projects_uuid_key" ON "app_projects"("uuid");
-- CreateIndex
CREATE UNIQUE INDEX "app_projects_uuid_key" ON "app_projects"("uuid");
-- -- CreateIndex
-- CREATE UNIQUE INDEX "app_metrics_uuid_key" ON "app_metrics"("uuid");
-- CreateIndex
CREATE UNIQUE INDEX "app_metrics_uuid_key" ON "app_metrics"("uuid");
-- -- AddForeignKey
-- ALTER TABLE "app_metrics" ADD CONSTRAINT "app_metrics_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "app_projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "app_metrics" ADD CONSTRAINT "app_metrics_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "app_projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "app_metrics_uuid_key";

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "app_metrics" ALTER COLUMN "date_requested" SET DATA TYPE DATE;

View File

@ -0,0 +1,32 @@
/*
Warnings:
- The primary key for the `app_metrics` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `app_metrics` table. All the data in the column will be lost.
- You are about to drop the column `projectId` on the `app_metrics` table. All the data in the column will be lost.
- The primary key for the `app_projects` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `app_projects` table. All the data in the column will be lost.
- A unique constraint covering the columns `[uuid]` on the table `app_metrics` will be added. If there are existing duplicate values, this will fail.
- Added the required column `projectUuid` to the `app_metrics` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "app_metrics" DROP CONSTRAINT "app_metrics_projectId_fkey";
-- AlterTable
ALTER TABLE "app_metrics" DROP CONSTRAINT "app_metrics_pkey",
DROP COLUMN "id",
DROP COLUMN "projectId",
ADD COLUMN "projectUuid" TEXT NOT NULL,
ADD CONSTRAINT "app_metrics_pkey" PRIMARY KEY ("uuid");
-- AlterTable
ALTER TABLE "app_projects" DROP CONSTRAINT "app_projects_pkey",
DROP COLUMN "id",
ADD CONSTRAINT "app_projects_pkey" PRIMARY KEY ("uuid");
-- CreateIndex
CREATE UNIQUE INDEX "app_metrics_uuid_key" ON "app_metrics"("uuid");
-- AddForeignKey
ALTER TABLE "app_metrics" ADD CONSTRAINT "app_metrics_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "app_projects"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,42 @@
export type IFunctionBinder = (next?: IFunctionBinder) => Promise<void>;
/**
* @description execute functions on body of another function
* @example `
* const binders = [fooA, fooB, fooC];
*
* //Will be something like
* function fooA(fooB, fooC, next) {
* //code...
* fooB(fooC, next);
* //code...
* }
* function fooB(fooC, next) {
* //code...
* fooC(next);
* //code...
* }
*
* function fooC(next) {
* //code...
* next();
* //code...
* }
* `
* fooC will be executed in body of fooB and fooB in body of fooA
*/
export default abstract class FunctionBinder {
public static async bind(next: () => Promise<any>, binders: IFunctionBinder[]) {
let currentBinder = async function () {
await next();
};
let index = binders.length;
while (index--) {
const localIndex = index;
const nextBinder = currentBinder;
currentBinder = async () => binders[localIndex]!(nextBinder);
}
await currentBinder();
}
}

View File

@ -1,7 +1,13 @@
import { plainToClassFromExist } from "class-transformer";
import { type ClassTransformOptions, plainToClass, plainToClassFromExist } from "class-transformer";
export default abstract class ObjectHydrate {
public static hydrate<T = { [key: string]: any }>(object: { [key: string]: any }, from: { [key: string]: any }): T {
return plainToClassFromExist(object, from) as T;
public static hydrate<T = {}>(object: T, from: Partial<T>, options?: ClassTransformOptions): T {
return plainToClassFromExist(object, from, options);
}
public static map<T = {}>(ClassEntity: { new (): T }, fromArray: Partial<T>[], options?: ClassTransformOptions): T[] {
return fromArray.map((from) => {
return plainToClass(ClassEntity, from, options);
});
}
}

View File

@ -0,0 +1,13 @@
export function getUUIDFromPath(path: string, re: RegExp): string {
let uuid = "";
const matches = path.match(re);
if (matches !== null) {
uuid = matches[matches.length - 1]!;
}
return uuid;
}
export function getRPCFromPath(basePath: string, path: string, re: RegExp): string {
return path.replace("/" + basePath + getUUIDFromPath(path, re), "");
}

View File

@ -0,0 +1,4 @@
export default abstract class BaseRepository {
protected readonly maxFetchRows = 100;
protected readonly defaultFetchRows = 50;
}

View File

@ -1,190 +0,0 @@
import TezosLink from "@Common/databases/TezosLink";
import { MetricEntity } from "@Common/ressources";
import { ORMBadQueryError } from "@Common/system/database/exceptions/ORMBadQueryError";
import { type Prisma } from "@prisma/client";
import { Service } from "typedi";
type RequestsByDayMetrics = {
date_requested: Date;
count: number;
};
@Service()
export default class MetricRepository {
constructor(private database: TezosLink) {}
protected get model() {
return this.database.getClient().metric;
}
protected get instanceDb() {
return this.database.getClient();
}
public async findMany(query: any): Promise<MetricEntity[]> {
try {
return this.model.findMany(query) as Promise<MetricEntity[]>;
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
public async findOne(metricEntity: Partial<MetricEntity>): Promise<Partial<MetricEntity> | null> {
try {
const data = { ...metricEntity };
return this.model.findUnique({ where: data });
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
public async create(metricEntity: Partial<MetricEntity>): Promise<MetricEntity> {
try {
const data = { ...metricEntity };
return this.model.create({
data: {
path: data.path!,
uuid: data.uuid!,
remote_address: data.remote_address!,
date_requested: data.date_requested!,
project: {
connect: {
id: data.id!,
},
},
},
}) as Promise<MetricEntity>;
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
// Create many metrics in bulk
public async createMany(metricEntity: Partial<MetricEntity[]>): Promise<MetricEntity[]> {
try {
const data = { ...metricEntity };
const result: MetricEntity[] = [];
this.instanceDb.$transaction(async(transaction: Prisma.TransactionClient) => {
for (const item of data) {
if (!item) continue;
result.push(
await transaction.metric.create({
data: {
path: item.path!,
uuid: item.uuid!,
remote_address: item.remote_address!,
date_requested: item.date_requested!,
project: {
connect: {
id: item.id!,
},
},
},
}),
);
}
});
return result;
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
// Count Rpc path usage for a specific project
public async countRpcPathUsage(projectId: number, from: Date, to: Date): Promise<any> {
try {
return this.model.groupBy({
by: ["path"],
_count: {
path: true,
},
where: {
projectId: projectId,
date_requested: {
gte: from,
lte: to,
},
},
});
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
// Last requests for a specific project
public async findLastRequests(projectId: number, limit: number): Promise<MetricEntity[]> {
try {
return this.model.findMany({
where: {
projectId: projectId,
},
take: limit,
orderBy: {
date_requested: "desc",
},
});
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
// Find Requests by Day for a specific project
public async findRequestsByDay(projectId: number, from: Date, to: Date): Promise<RequestsByDayMetrics[]> {
try {
const result: RequestsByDayMetrics[] = [];
const response = this.model.groupBy({
by: ["date_requested"],
_count: {
date_requested: true,
},
where: {
projectId: projectId,
date_requested: {
gte: from,
lte: to,
},
},
});
for (const item of response as Array<{ date_requested: Date; _count: { date_requested: number } }> | any) {
result.push({
date_requested: item.date_requested,
count: item._count.date_requested,
});
}
return result;
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
// Count all metrics by criterias for a specific project
public async countAll(projectId: number): Promise<number> {
try {
return this.model.count({
where: {
projectId: projectId,
},
});
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
// Remove Three months old metrics
public async removeOldMetricsBymonths(months: number): Promise<void> {
try {
const date = new Date();
date.setMonth(date.getMonth() - months);
this.model.deleteMany({
where: {
date_requested: {
lte: date,
},
},
});
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
}

View File

@ -1,62 +0,0 @@
import TezosLink from "@Common/databases/TezosLink";
import { ProjectEntity } from "@Common/ressources";
import { ORMBadQueryError } from "@Common/system/database/exceptions/ORMBadQueryError";
import { Service } from "typedi";
import { v4 as uuidv4 } from "uuid";
@Service()
export default class ProjectRepository {
constructor(private database: TezosLink) {}
protected get model() {
return this.database.getClient().project;
}
public async findMany(query: any): Promise<ProjectEntity[]> {
try {
return this.model.findMany(query) as Promise<ProjectEntity[]>;
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
public async findOne(projectEntity: Partial<ProjectEntity>): Promise<Partial<ProjectEntity> | null> {
try {
const data = { ...projectEntity };
return this.model.findUnique({
where: data,
include: {
// Include metrics & count
Metrics: true,
_count: {
select: { Metrics: true },
},
},
});
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
public async create(projectEntity: Partial<ProjectEntity>): Promise<ProjectEntity> {
try {
const data = { ...projectEntity };
data.uuid = uuidv4();
return this.model.create({
data: {
uuid: data.uuid,
title: data.title!,
network: data.network!,
},
include: {
// Include metrics
Metrics: true,
},
}) as Promise<ProjectEntity>;
} catch (error) {
throw new ORMBadQueryError((error as Error).message, error as Error);
}
}
}

View File

@ -0,0 +1,220 @@
// import TezosLink from "@Common/databases/TezosLink";
// import ObjectHydrate from "@Common/helpers/ObjectHydrate";
// import { MetricEntity } from "@Common/ressources";
// import { ORMBadQueryError } from "@Common/system/database/exceptions/ORMBadQueryError";
// import { type Prisma } from "@prisma/client";
import { Service } from "typedi";
// import { v4 as uuidv4 } from "uuid";
import BaseRepository from "../BaseRepository";
export class RequestsByDayMetrics {
date_requested!: Date;
count!: number;
}
export class CountRpcPathUsage {
path!: string;
count!: number;
}
@Service()
export default class MetricsRepository extends BaseRepository {
// constructor(private database: TezosLink) {
// super();
// }
// protected get model() {
// return this.database.getClient().metric;
// }
// protected get instanceDb() {
// return this.database.getClient();
// }
// public async findMany(query: Prisma.MetricFindManyArgs): Promise<MetricEntity[]> {
// try {
// // Use Math.min to limit the number of rows fetched
// const limit = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
// // Update the query with the limited limit
// const metrics = await this.model.findMany({ ...query, take: limit });
// return ObjectHydrate.map<MetricEntity>(MetricEntity, metrics, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// public async findOne(metricEntity: Partial<MetricEntity>): Promise<Partial<MetricEntity> | null> {
// try {
// const metric = await this.model.findUnique({ where: metricEntity });
// return ObjectHydrate.hydrate<MetricEntity>(new MetricEntity(), metric, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// public async create(metricEntity: Partial<MetricEntity>): Promise<MetricEntity> {
// try {
// const data = { ...metricEntity };
// data.uuid = uuidv4();
// const metric = (await this.model.create({
// data: {
// path: data.path!,
// uuid: data.uuid!,
// remote_address: data.remote_address!,
// date_requested: data.date_requested!,
// project: {
// connect: {
// uuid: data.project!.uuid!,
// },
// },
// },
// })) as MetricEntity;
// return ObjectHydrate.hydrate<MetricEntity>(new MetricEntity(), metric, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// // Create many metrics in bulk
// public async createMany(metricEntity: Partial<MetricEntity[]>): Promise<MetricEntity[]> {
// try {
// const result: MetricEntity[] = [];
// this.instanceDb.$transaction(async (transaction: Prisma.TransactionClient) => {
// for (const item of metricEntity) {
// if (!item) continue;
// const data = { ...item };
// data.uuid = uuidv4();
// result.push(
// await transaction.metric.create({
// data: {
// path: data.path!,
// uuid: data.uuid!,
// remote_address: data.remote_address!,
// date_requested: data.date_requested!,
// project: {
// connect: {
// uuid: data.projectUuid!,
// },
// },
// },
// }),
// );
// }
// });
// return ObjectHydrate.map<MetricEntity>(MetricEntity, result, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// // Count Rpc path usage for a specific project
// public async countRpcPathUsage(ProjectUuid: string, from: Date, to: Date): Promise<CountRpcPathUsage[]> {
// try {
// const result: CountRpcPathUsage[] = [];
// const response = await this.model.groupBy({
// by: ["path"],
// _count: {
// path: true,
// },
// where: {
// projectUuid: ProjectUuid,
// date_requested: {
// gte: from,
// lte: to,
// },
// },
// });
// response.forEach((item) => {
// result.push({
// path: item.path,
// count: item._count.path,
// });
// });
// return ObjectHydrate.map<CountRpcPathUsage>(CountRpcPathUsage, response, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// // Last requests for a specific project
// public async findLastRequests(projectUuid: string, limit: number): Promise<MetricEntity[]> {
// try {
// // Use Math.min to limit the number of rows fetched
// const rows = Math.min(limit || this.defaultFetchRows, this.maxFetchRows);
// const metrics = await this.model.findMany({
// where: {
// projectUuid: projectUuid,
// },
// take: rows,
// orderBy: {
// date_requested: "desc",
// },
// });
// return ObjectHydrate.map<MetricEntity>(MetricEntity, metrics, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// // Find Requests by Day for a specific project
// public async findRequestsByDay(projectUuid: string, from: Date, to: Date): Promise<RequestsByDayMetrics[]> {
// try {
// const result: RequestsByDayMetrics[] = [];
// const response = await this.model.groupBy({
// by: ["date_requested"],
// _count: {
// date_requested: true,
// },
// where: {
// projectUuid: projectUuid,
// date_requested: {
// gte: from,
// lte: to,
// },
// },
// });
// response.forEach((item) => {
// result.push({
// date_requested: item.date_requested,
// count: item._count.date_requested,
// });
// });
// return ObjectHydrate.map<RequestsByDayMetrics>(RequestsByDayMetrics, result, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// // Count all metrics by criterias for a specific project
// public async countAll(projectUuid: string): Promise<number> {
// try {
// return this.model.count({
// where: {
// projectUuid: projectUuid,
// },
// }) as Promise<number>;
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
// // Remove Three months old metrics
// public async removeOldMetricsBymonths(months: number): Promise<void> {
// try {
// const date = new Date();
// date.setMonth(date.getMonth() - months);
// this.model.deleteMany({
// where: {
// date_requested: {
// lte: date,
// },
// },
// });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
}

View File

@ -0,0 +1,63 @@
// import TezosLink from "@Common/databases/TezosLink";
// import ObjectHydrate from "@Common/helpers/ObjectHydrate";
import { ProjectEntity } from "@Common/ressources";
// import { ORMBadQueryError } from "@Common/system/database/exceptions/ORMBadQueryError";
import { Service } from "typedi";
// import { v4 as uuidv4 } from "uuid";
import BaseRepository from "../BaseRepository";
@Service()
export default class ProjectsRepository extends BaseRepository {
// constructor(private database: TezosLink) {
// super();
// }
// protected get model() {
// return this.database.getClient().project;
// }
// public async findMany(query: Prisma.ProjectFindManyArgs): Promise<ProjectEntity[]> {
// try {
// // Use Math.min to limit the number of rows fetched
// const limit = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
// // Update the query with the limited limit
// const projects = await this.model.findMany({ ...query, take: limit });
// return ObjectHydrate.map<ProjectEntity>(ProjectEntity, projects, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
// }
public async findOne(projectEntity: Partial<ProjectEntity>) {
// try {
// const project = (await this.model.findFirst({
// where: projectEntity,
// })) as ProjectEntity;
// return ObjectHydrate.hydrate<ProjectEntity>(new ProjectEntity(), project, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
}
public async create(projectEntity: Partial<ProjectEntity>) {
// try {
// const data = { ...projectEntity };
// data.uuid = uuidv4();
// const project = (await this.model.create({
// data: {
// uuid: data.uuid,
// title: data.title!,
// network: data.network!,
// },
// include: {
// // Include metrics
// Metrics: true,
// },
// })) as ProjectEntity;
// return ObjectHydrate.hydrate<ProjectEntity>(new ProjectEntity(), project, { strategy: "exposeAll" });
// } catch (error) {
// throw new ORMBadQueryError((error as Error).message, error as Error);
// }
}
}

View File

@ -1,31 +1,36 @@
import { IsNotEmpty, IsDate } from "class-validator";
import ProjectEntity from "./ProjectEntity";
export default class MetricEntity {
@IsNotEmpty()
public id!: number;
@IsNotEmpty(({groups: ["create"]}))
public path!: string;
@IsNotEmpty(({groups: ["create"]}))
public uuid!: string;
@IsNotEmpty(({groups: ["create"]}))
public path!: string;
public remote_address!: string;
@IsNotEmpty(({groups: ["create"]}))
public date_requested!: Date;
@IsNotEmpty(({groups: ["create"]}))
public projectId!: number;
public projectUuid!: string;
@IsNotEmpty(({groups: ["create"]}))
public project!: ProjectEntity;
@IsDate()
public createdAt?: Date;
@IsDate()
public updatedAt?: Date;
set remoteAddress(remote_address: string) {
this.remote_address = remote_address;
}
get remoteAddress() {
return this.remote_address;
}
set dateRequested(date_requested: Date) {
this.date_requested = date_requested;
}
get dateRequested() {
return this.date_requested;
}
}

View File

@ -1,25 +1,19 @@
import { IsNotEmpty, IsOptional, IsDate } from "class-validator";
import { IsNotEmpty } from "class-validator";
import MetricEntity from "./MetricEntity";
export default class ProjectEntity {
@IsNotEmpty()
public id!: number;
public uuid!: string;
@IsNotEmpty({ groups: ["create"] })
public title!: string;
@IsNotEmpty()
public uuid!: string;
@IsDate()
public createdAt!: Date;
@IsDate()
public updatedAt!: Date;
@IsNotEmpty({ groups: ["create"] })
public network!: string;
@IsOptional()
public metrics?: MetricEntity[];
}

View File

@ -1,2 +1,11 @@
export default class BaseService {}
import { BackendVariables } from "@Common/config/variables/Variables";
import Container from "typedi";
export default abstract class BaseService {
/** @TODO place methods in a config file */
public static readonly whitelisted: string[] = ["/chains/main/blocks"];
public static readonly blacklisted: string[] = ["/context/contracts", "/monitor", "/network"];
public static readonly rollingPatterns: string[] = ["/head", "/injection/operation"];
public static readonly network: string = Container.get(BackendVariables).TEZOS_NETWORK;
}

View File

@ -1,88 +0,0 @@
import ObjectHydrate from "@Common/helpers/ObjectHydrate";
import MetricRepository from "@Common/repositories/MetricsRepository";
import { MetricEntity } from "@Common/ressources";
import { type processFindManyQuery } from "prisma-query";
import { Service } from "typedi";
@Service()
export default class MetricService {
constructor(private metricRepository: MetricRepository) {}
/**
* @throws {Error} If metrics are undefined
*/
public async getByCriterias(query: ReturnType<typeof processFindManyQuery>) {
const metrics = await this.metricRepository.findMany(query);
return ObjectHydrate.hydrate<Partial<MetricEntity>>(new MetricEntity(), metrics);
}
/**
* @throws {Error} If metric is undefined
*/
public async getByUUID(metricEntity: Partial<MetricEntity>) {
const metric = await this.metricRepository.findOne(metricEntity);
if (!metric) return null;
return ObjectHydrate.hydrate<Partial<MetricEntity>>(new MetricEntity(), metric);
}
/**
*
* @throws {Error} If metric cannot be created
* @returns
*/
public async create(metricEntity: Partial<MetricEntity>) {
const metric = await this.metricRepository.create(metricEntity);
if (!metric) return null;
return ObjectHydrate.hydrate<Partial<MetricEntity>>(new MetricEntity(), metric);
}
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
public async getCountRpcPath(metricEntity: Partial<MetricEntity>, from: Date, to: Date) {
const pathsCount = await this.metricRepository.countRpcPathUsage(metricEntity.projectId!,from,to);
if (!pathsCount) return null;
return pathsCount;
}
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
public async getCountAllMetrics(metricEntity: Partial<MetricEntity>) {
const count = await this.metricRepository.countAll(metricEntity.projectId!);
if (isNaN(count)) return null;
return count;
}
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
public async getLastMetrics(metricEntity: Partial<MetricEntity>, limit: number){
const lastMetric = await this.metricRepository.findLastRequests(metricEntity.projectId!,limit);
return ObjectHydrate.hydrate<Partial<MetricEntity>>(new MetricEntity(), lastMetric);
}
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
public async getRequestsByDay(metricEntity: Partial<MetricEntity>, from: Date, to: Date){
const requestByDay = await this.metricRepository.findRequestsByDay(metricEntity.projectId!,from,to);
return ObjectHydrate.hydrate<Partial<MetricEntity>>(new MetricEntity(), requestByDay);
}
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
public async removeThreeMontsOldMetrics() {
const months = 3;
await this.metricRepository.removeOldMetricsBymonths(months);
}
}

View File

@ -0,0 +1,76 @@
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
@Service()
export default class MetricsService extends BaseService {
constructor() {
super();
}
// /**
// * @throws {Error} If metrics are undefined
// */
// public async getByCriterias(query: ReturnType<typeof processFindManyQuery>): Promise<MetricEntity[]> {
// return await this.metricRepository.findMany(query);
// }
// /**
// *
// * @throws {Error} If metric cannot be created
// * @returns
// */
// public async create(metricEntity: Partial<MetricEntity>): Promise<Partial<MetricEntity>> {
// const metric = await this.metricRepository.create(metricEntity);
// if (!metric) return Promise.reject(new Error("Cannot create metric"));
// return metric;
// }
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
// public async getCountRpcPath(uuid: string, from: Date, to: Date): Promise<CountRpcPathUsage[]> {
// const pathsCount = await this.metricRepository.countRpcPathUsage(uuid, from, to);
// if (!pathsCount) return Promise.reject(new Error("Cannot get count of rpc path"));
// return pathsCount;
// }
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
// public async getCountAllMetrics(metricEntity: Partial<MetricEntity>): Promise<number> {
// const count = await this.metricRepository.countAll(metricEntity.uuid!);
// if (isNaN(count)) Promise.reject(new Error("Cannot get count of metrics"));
// return count;
// }
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
// public async getLastMetrics(uuid: string, limit: number): Promise<MetricEntity[]> {
// return await this.metricRepository.findLastRequests(uuid, limit);
// }
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
// public async getRequestsByDay(uuid: string, from: Date, to: Date): Promise<RequestsByDayMetrics[]> {
// return await this.metricRepository.findRequestsByDay(uuid, from, to);
// }
/**
*
* @throws {Error} If metric is undefined
* @returns
*/
// public async removeThreeMontsOldMetrics(): Promise<void> {
// const months = 3;
// await this.metricRepository.removeOldMetricsBymonths(months);
// }
}

View File

@ -1,39 +0,0 @@
import ObjectHydrate from "@Common/helpers/ObjectHydrate";
import ProjectRepository from "@Common/repositories/ProjectRepository";
import { ProjectEntity } from "@Common/ressources";
import { type processFindManyQuery } from "prisma-query";
import { Service } from "typedi";
@Service()
export default class ProjectService {
constructor(private projectRepository: ProjectRepository) {}
/**
* @throws {Error} If projects are undefined
*/
public async getByCriterias(query: ReturnType<typeof processFindManyQuery>) {
const projects = await this.projectRepository.findMany(query);
return ObjectHydrate.hydrate<Partial<ProjectEntity>>(new ProjectEntity(), projects);
}
/**
* @throws {Error} If project is undefined
*/
public async getByUUID(projectEntity: Partial<ProjectEntity>) {
const project = await this.projectRepository.findOne(projectEntity);
if (!project) return null;
return ObjectHydrate.hydrate<Partial<ProjectEntity>>(new ProjectEntity(), project);
}
/**
*
* @throws {Error} If project cannot be created
* @returns
*/
public async create(projectEntity: Partial<ProjectEntity>) {
const project = await this.projectRepository.create(projectEntity);
if (!project) return null;
return ObjectHydrate.hydrate<Partial<ProjectEntity>>(new ProjectEntity(), project);
}
}

View File

@ -0,0 +1,37 @@
// import ProjectsRepository from "@Common/repositories/projects/ProjectsRepository";
import { ProjectEntity } from "@Common/ressources";
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
@Service()
export default class ProjectsService extends BaseService {
constructor() {
super();
}
/**
* @throws {Error} If projects are undefined
*/
// public async getByCriterias(query: ReturnType<typeof processFindManyQuery>): Promise<ProjectEntity[]> {
// return this.projectRepository.findMany(query);
// }
/**
* @throws {Error} If project is undefined
*/
public async getByUUID(projectEntity: Partial<ProjectEntity>){
// const project = await this.projectRepository.findOne(projectEntity);
// if (!project) Promise.reject(new Error("Cannot get project by uuid"));
// return project;
}
/**
*
* @throws {Error} If project cannot be created
* @returns
*/
public async create(projectEntity: Partial<ProjectEntity>){
// const project = await this.projectRepository.create(projectEntity);
// if (!project) Promise.reject(new Error("Cannot create project"));
// return project;
}
}

View File

@ -0,0 +1,119 @@
import MetricEntity from "@Common/ressources/MetricEntity";
import HttpCodes from "@Common/system/controller-pattern/HttpCodes";
import { IHttpReponse, IStatusNode } from "@Common/system/interfaces/Interfaces";
import BaseService from "@Services/BaseService";
import axios from "axios";
import { IsNotEmpty, IsUUID, Validate } from "class-validator";
import { Service } from "typedi";
import IsRpcPathAllowed from "./validators/IsRpcPathAllowed";
import IsValidProject from "./validators/IsValidProject";
import { BackendVariables } from "@Common/config/variables/Variables";
import ProjectsRepository from "@Common/repositories/projects/ProjectsRepository";
export class RpcRequest {
@IsNotEmpty()
@Validate(IsRpcPathAllowed)
path!: string;
@IsNotEmpty()
@IsUUID()
@Validate(IsValidProject, [{ network: BaseService.network }])
uuid!: string;
@IsNotEmpty()
remoteAddress!: string;
}
@Service()
export default class ProxyService extends BaseService {
constructor(private projectRepository: ProjectsRepository, private variables: BackendVariables) {
super();
}
/**
* @throws {Error} if url is undefined
*/
public getHttpServerResponse(): IHttpReponse {
return {
status: HttpCodes.SUCCESS,
reason: null,
} as IHttpReponse;
}
/**
* @throws {Error} if url is undefined
*/
public async getNodesStatus(): Promise<IStatusNode> {
const archiveTestURL = new URL(`${this.variables.ARCHIVE_NODES_URL}/chains/main/blocks/head`);
const rollingTestURL = new URL(`${this.variables.ROLLING_NODES_URL}/chains/main/blocks/head`);
const archive_node = {
status: HttpCodes.INTERNAL_ERROR,
reason: null,
} as IHttpReponse;
const rolling_node = {
status: HttpCodes.INTERNAL_ERROR,
reason: null,
} as IHttpReponse;
const [archive, rolling] = await Promise.allSettled([axios.get(archiveTestURL.toString()), axios.get(rollingTestURL.toString())]);
if (archive.status === "fulfilled") archive_node.status = archive.value.status;
if (archive.status === "rejected") archive_node.reason = archive.reason;
if (rolling.status === "fulfilled") rolling_node.status = rolling.value.status;
if (rolling.status === "rejected") rolling_node.reason = rolling.reason;
return {
archive_node,
rolling_node,
};
}
/**
*
* @throws {Error} If metric cannot be created
* @returns
*/
// async saveMetric(metricEntity: Partial<MetricEntity>) {
// const metric = await this.metricsRepository.create(metricEntity);
// if (!metric) return null;
// return metric;
// }
// Proxy proxy an http request to the right repositories
public async proxy(request: RpcRequest): Promise<string> {
console.info(`Received proxy request for path: ${request.path}`);
const project = await this.projectRepository.findOne({ uuid: request.uuid, network: BaseService.network });
// if (!project) {
// return Promise.reject(`Project uuid: ${request.uuid} with network: ${BaseService.network} does not exist`);
// }
let response = "";
if (this.isRollingNodeRedirection(request.path)) {
console.info("Forwarding request directly to rolling node (as a reverse proxy)");
const rollingURL = new URL(`${this.variables.ROLLING_NODES_URL}/${request.path}`);
const { data } = await axios.get(rollingURL.toString());
response = data;
} else {
console.info("Forwarding request directly to archive node (as a reverse proxy)");
const archiveURL = new URL(`${this.variables.ARCHIVE_NODES_URL}/${request.path}`);
const { data } = await axios.get(archiveURL.toString());
response = data;
}
// Logger les metrics
const metric = new MetricEntity();
Object.assign(metric, request, { project, dateRequested: new Date() });
// await this.saveMetric(metric);
return response;
}
isRollingNodeRedirection(url: string): boolean {
const pureUrl = `/${url!.trim()}`;
console.info(`Checking if ${pureUrl} is a rolling node redirection`);
return Boolean(BaseService.rollingPatterns.find((rollingpattern) => pureUrl.includes(rollingpattern)));
}
}

View File

@ -0,0 +1,31 @@
import BaseService from "@Services/BaseService";
import { ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
@ValidatorConstraint({ name: "IsRpcPathAllowed" })
export default class IsRpcPathAllowed implements ValidatorConstraintInterface {
public validate(path: string) {
return isAllowed(path);
}
public defaultMessage() {
return `not a valid path!`;
}
}
function isAllowed(path: string): boolean {
const pureUrl = `/${path!.trim()}`;
let nonWhitelistedPart = "";
for (const whitelistPath of BaseService.whitelisted) {
if (pureUrl.includes(whitelistPath)) {
nonWhitelistedPart = pureUrl.slice(pureUrl.indexOf(whitelistPath) + whitelistPath.length);
break;
}
}
for (const blacklistPath of BaseService.blacklisted) {
if (nonWhitelistedPart.includes(blacklistPath) || pureUrl.includes(blacklistPath)) {
return false;
}
}
return true;
}

View File

@ -0,0 +1,24 @@
import BaseService from "@Services/BaseService";
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import Container from "typedi";
import { RpcRequest } from "../ProxyService";
import ProjectsRepository from "@Common/repositories/projects/ProjectsRepository";
@ValidatorConstraint({ name: "IsValidProject" })
export default class IsValidProject implements ValidatorConstraintInterface {
public async validate(uuid: string, args: ValidationArguments) {
const projectRepository = Container.get(ProjectsRepository);
if (args.constraints?.[0]?.network) {
return Boolean(await projectRepository.findOne({ uuid, network: BaseService.network }));
}
return false;
}
public defaultMessage(args: ValidationArguments) {
const network = args.constraints?.[0]!.network;
const uuid = (args.object as RpcRequest).uuid;
return `Project uuid: ${uuid} with network: ${network} does not exist`;
}
}

View File

@ -1,20 +1,22 @@
import dotenv from "dotenv";
import { PrismaClient } from "@prisma/client";
import IDatabaseConfig from "../../config/IDatabaseConfig";
import IDatabaseConfig from "../../config/database/IDatabaseConfig";
import { BackendVariables } from "@Common/config/variables/Variables";
import Container from "typedi";
dotenv.config();
export default class DbProvider {
protected readonly variables = Container.get(BackendVariables);
protected client = new PrismaClient({
datasources: {
db: {
url: `postgres://${process.env["DATABASE_USER"]}:${process.env["DATABASE_PASSWORD"]}@${process.env["DATABASE_HOSTNAME"]}:${process.env["DATABASE_PORT"]}/${process.env["DATABASE_NAME"]}`,
url: `postgres://${this.variables.DATABASE_USER}:${this.variables.DATABASE_PASSWORD}@${this.variables.DATABASE_HOSTNAME}:${this.variables.DATABASE_PORT}/${this.variables.DATABASE_NAME}`,
},
},
});
constructor(protected config: IDatabaseConfig) {
}
constructor(protected config: IDatabaseConfig) {}
public async connect(): Promise<void> {
await this.client.$connect();

View File

@ -1,4 +1,4 @@
import IDatabaseConfig from "@Common/config/IDatabaseConfig";
import IDatabaseConfig from "@Common/config/database/IDatabaseConfig";
import DbProvider from "./DbProvider";
export type { IDatabaseConfig };

View File

@ -0,0 +1,9 @@
export interface IHttpReponse {
status: number;
reason: string | null;
}
export interface IStatusNode {
archive_node: IHttpReponse;
rolling_node: IHttpReponse;
}

View File

@ -1,36 +1,34 @@
import "module-alias/register";
import "reflect-metadata";
import dotenv from "dotenv";
import { Container } from "typedi";
import ExpressServer from "@Common/system/ExpressServer";
import routes from "@Api/controllers/index";
import cors from "cors";
import bodyParser from "body-parser";
import TezosLink from "@Common/databases/TezosLink";
// import TezosLink from "@Common/databases/TezosLink";
import errorHandler from "@Api/middlewares/ErrorHandler";
import { BackendVariables } from "@Common/config/variables/Variables";
dotenv.config();
(async () => {
try {
const variables = await Container.get(BackendVariables).validate();
const port = process.env["NEXT_PUBLIC_API_PORT"];
const rootUrl = process.env["NEXT_PUBLIC_API_ROOT_URL"];
const label = process.env["NEXT_PUBLIC_API_LABEL"] ?? "Unknown Service";
const port = variables.API_PORT;
const rootUrl = variables.API_ROOT_URL;
const label = variables.API_LABEL ?? "Unknown Service";
if (!port) throw new Error(`process.env Port is undefined`);
if (!rootUrl) throw new Error(`process.env RootUrl is undefined`);
Container.get(TezosLink).connect();
// Container.get(TezosLink).connect();
Container.get(ExpressServer).init({
label,
port: parseInt(port),
rootUrl,
middlwares: [
cors({ origin: "*" }),
bodyParser.urlencoded({ extended: true }),
bodyParser.json(),
],
middlwares: [cors({ origin: "*" }), bodyParser.urlencoded({ extended: true }), bodyParser.json()],
errorHandler,
});
routes.start();
} catch (e) {
console.error(e);
}
})();

View File

@ -1,21 +1,27 @@
import "module-alias/register";
import "reflect-metadata";
import dotenv from "dotenv";
import { Container } from "typedi";
import NextServer from "@Common/system/NextJs";
import dotenv from "dotenv";
import { FrontendVariables } from "@Front/Config/VariablesFront";
(async () => {
try {
dotenv.config();
const frontVariables = Container.get(FrontendVariables);
const port = process.env["WEB_PORT"];
const rootUrl = process.env["WEB_ROOT_URL"];
const label = process.env["WEB_LABEL"] ?? "Unknown Service";
if (!port) throw new Error(`process.env Port is undefined`);
if (!rootUrl) throw new Error(`process.env RootUrl is undefined`);
const port = frontVariables.WEB_PORT;
const rootUrl = frontVariables.WEB_ROOT_URL;
const label = frontVariables.WEB_LABEL ?? "Unknown Service";
Container.get(NextServer).init({
label,
isDev: process.env.NODE_ENV !== 'production',
isDev: frontVariables.NODE_ENV !== "production",
port: parseInt(port),
rootUrl,
});
} catch (e) {
console.error(e);
}
})();

View File

@ -10,7 +10,7 @@
background-color: transparent;
width: 16px;
height: 16px;
border: 1px solid $green-flash;
border: 1px solid var(green-flash);
border-radius: 2px;
margin-right: 16px;
display: grid;
@ -23,7 +23,7 @@
display: grid;
width: 16px;
height: 16px;
background-color: $green-flash;
background-color: var(green-flash);
border-radius: 2px;
transform: scale(0);
}

View File

@ -2,7 +2,16 @@
.root {
position: relative;
textarea{
resize: none;
height: auto;
box-sizing: border-box;
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-size: 18px;
line-height: 22px;
}
.input {
z-index: 1;
display: flex;
@ -10,7 +19,6 @@
align-items: center;
padding: 24px;
gap: 10px;
width: 530px;
height: 70px;
border: 1px solid $grey-medium;
@ -18,7 +26,7 @@
&:focus {
~ .fake-placeholder {
transform: translateY(-35px);
transition: transform 0.5s ease;
transition: transform 0.3s ease-in-out;
}
}
&:not([value=""]) {
@ -32,7 +40,7 @@
&:focus {
~ .fake-placeholder {
transform: translateY(-35px);
transition: transform 0.5s ease;
transition: transform 0.3s ease-in-out;
}
}
&:not([value=""]) {
@ -46,7 +54,7 @@
&:not([value=""]) {
~ .fake-placeholder {
transform: translateY(-35px);
transition: transform 0.5s ease;
transition: transform 0.3s ease-in-out;
}
}

View File

@ -7,7 +7,7 @@ import classes from "./classes.module.scss";
export type IProps = IBaseFieldProps & {
fakeplaceholder: string;
large?: boolean;
textarea?: boolean;
};
// @ts-ignore TODO: typing error on IProps (validator class?? cf Massi 22/02/23)
@ -30,7 +30,7 @@ export default class InputField extends BaseField<IProps> {
// we always need to control the input so we need to set the value as "" by default
const value = this.state.value ?? "";
if (this.props.large === true) {
if (this.props.textarea === true) {
return (
<Typography typo={ITypo.NAV_INPUT_16} color={ITypoColor.GREY}>
<div className={classes["root"]}>

View File

@ -11,6 +11,16 @@
position: absolute;
top: 107px;
right: 56px;
animation: smooth-appear 0.2s ease forwards;
@keyframes smooth-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.notification-header {
width: 100%;

View File

@ -10,6 +10,17 @@
top: 107px;
right: 66px;
text-align: center;
animation: smooth-appear 0.2s ease forwards;
@keyframes smooth-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
> *:not(:last-child) {
margin-bottom: 24px;
}

View File

@ -18,6 +18,16 @@
width: 100%;
height: 100%;
background-color: $modal-background;
animation: smooth-appear 0.2s ease forwards;
@keyframes smooth-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
.container {
@ -28,6 +38,16 @@
box-shadow: 0px 6px 12px rgba(255, 255, 255, 0.11);
overflow: auto;
padding: 32px;
animation: smooth-appear 0.2s ease forwards;
@keyframes smooth-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (max-width: $screen-s) {
width: 90%;

View File

@ -11,7 +11,7 @@
background-color: transparent;
width: 16px;
height: 16px;
border: 1px solid $green-flash;
border: 1px solid var(green-flash);
border-radius: 100px;
margin-right: 16px;
display: grid;
@ -24,7 +24,7 @@
content: "";
width: 10px;
height: 10px;
background-color: $green-flash;
background-color: var(green-flash);
border-radius: 100px;
transform: scale(0);
}

View File

@ -31,7 +31,7 @@
border-radius: 5px;
animation-name: slide-left;
animation-duration: 400ms;
animation-duration: 300ms;
animation-timing-function: $custom-easing;
animation-fill-mode: forwards;

View File

@ -1,8 +1,8 @@
import React from "react";
import Image from "next/image";
import ToolTipIcon from "@Assets/icons/tool-tip.svg";
import TooltipContent from "./Content";
import Typography, { ITypo } from "../Typography";
import TooltipMUI, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip";
import styled from "@emotion/styled";
type IProps = {
title?: string | JSX.Element;
@ -16,6 +16,19 @@ type IState = {
event: React.MouseEvent<HTMLElement> | null;
};
const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
<TooltipMUI {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: "var(--colormerdum)",
color: "var(--colormerdum)",
//boxShadow: theme.shadows[1],
fontSize: 11,
},[`& .${tooltipClasses.arrow}`]: {
// color: theme.palette.common.black,
},
}));
export default class Tooltip extends React.Component<IProps, IState> {
static defaultProps: Partial<IProps> = {
isNotFlex: false,
@ -23,44 +36,17 @@ export default class Tooltip extends React.Component<IProps, IState> {
public constructor(props: IProps) {
super(props);
this.state = {
showContent: false,
event: null,
};
this.togglePopup = this.togglePopup.bind(this);
this.moovePopup = this.moovePopup.bind(this);
}
public override render(): JSX.Element {
return (
<>
<LightTooltip title={this.props.text} placement="top" arrow>
<span
className={this.props.className}
style={!this.props.isNotFlex ? { display: "flex" } : {}}
onMouseEnter={this.togglePopup}
onMouseLeave={this.togglePopup}
onMouseMove={this.moovePopup}>
style={!this.props.isNotFlex ? { display: "flex" } : {}}>
<Image src={ToolTipIcon} alt="toolTip icon" />
</span>
<Typography typo={ITypo.CAPTION_14}>
<TooltipContent title={this.props.title} event={this.state.event} display={this.state.showContent}>
{this.props.text}
</TooltipContent>
</Typography>
</>
</LightTooltip>
);
}
private togglePopup(e: React.MouseEvent<HTMLSpanElement>): void {
this.setState({
showContent: !this.state.showContent,
event: e,
});
}
private moovePopup(e: React.MouseEvent<HTMLSpanElement>): void {
this.setState({
event: e,
});
}
}

View File

@ -1,6 +1,5 @@
import Head from "next/head";
import { ReactNode } from "react";
type DefaultLayoutProps = { children: ReactNode };
export const DefaultLayout = ({ children }: DefaultLayoutProps) => {

View File

@ -121,7 +121,7 @@ export default class DesignSystem extends BasePage<IProps, IState> {
<InputField name="input field" fakeplaceholder="input place hodler" />
</div>
<div className={classes["sub-section"]}>
<InputField name="input field" fakeplaceholder="text area place hodler" large={true} />
<InputField name="input field" fakeplaceholder="text area place hodler" textarea />
</div>
<div className={classes["sub-section"]}>
<InputField name="input field" fakeplaceholder="number place hodler" type="number" />

View File

@ -0,0 +1,37 @@
import { Service } from "typedi";
@Service()
export class FrontendVariables {
private static instance: FrontendVariables;
public readonly WEB_LABEL: string;
public readonly WEB_PORT!: string;
public readonly WEB_ROOT_URL!: string;
public readonly NEXT_PUBLIC_API_URL!: string;
public readonly NEXT_PUBLIC_RPC_GATEWAY_MAINNET_URL!: string;
public readonly NEXT_PUBLIC_RPC_GATEWAY_TESTNET_URL!: string;
public readonly NODE_ENV!: string;
constructor() {
this.NODE_ENV = process.env["NODE_ENV"]!;
this.WEB_LABEL = process.env["WEB_LABEL"]!;
this.WEB_PORT = process.env["WEB_PORT"]!;
this.WEB_ROOT_URL = process.env["WEB_ROOT_URL"]!;
this.NEXT_PUBLIC_API_URL = process.env["NEXT_PUBLIC_API_URL"]!;
this.NEXT_PUBLIC_RPC_GATEWAY_MAINNET_URL = process.env["NEXT_PUBLIC_RPC_GATEWAY_MAINNET_URL"]!;
this.NEXT_PUBLIC_RPC_GATEWAY_TESTNET_URL = process.env["NEXT_PUBLIC_RPC_GATEWAY_TESTNET_URL"]!;
}
public static getInstance(): FrontendVariables {
if (!this.instance) {
this.instance = new this();
}
return this.instance;
}
}

View File

@ -6,5 +6,29 @@
--root-padding: 64px 120px;
--font-primary: "Inter", sans-serif;
--font-secondary: "Source Sans Pro", sans-serif;
--colormerdum: blue;
--green-flash: $green-flash;
--blue-flash: $blue-flash;
--turquoise-flash: $turquoise-flash;
--purple-flash: $purple-flash;
--purple-hover: $purple-hover;
--orange-flash: $orange-flash;
--red-flash: $red-flash;
--re-hover: $re-hover;
--pink-flash: $pink-flash;
--pink-hover: $pink-hover;
--green-soft: $green-soft;
--blue-soft: $blue-soft;
--turquoise-soft: $turquoise-soft;
--purple-soft: $purple-soft;
--orange-soft: $orange-soft;
--red-soft: $red-soft;
--pink-soft: $pink-soft;
--grey: $grey;
--grey-medium: $grey-medium;
--grey-soft: $grey-soft;
}

View File

@ -1,5 +1,6 @@
@import "@Themes/constants.scss";
@import "@Themes/fonts.scss";
@import "@Themes/variables.scss";
* {
box-sizing: border-box;