feat: add nestjs replacement, remove nestjs (#285)

* feat: add nestjs replacement, remove nestjs

* fix: format issues

* fix: dependency issues

* fix: dependency issues

* fix: format issue

* fix: wrong channel used for logging channel
This commit is contained in:
Meier Lukas
2024-04-04 18:31:40 +02:00
committed by GitHub
parent c82915c6dc
commit 1936596c04
38 changed files with 599 additions and 2197 deletions

View File

@@ -1,73 +0,0 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@@ -1,4 +0,0 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

View File

@@ -1,59 +0,0 @@
{
"name": "@homarr/nest",
"version": "0.1.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "pnpm with-env vite",
"prebuild": "rimraf dist",
"build": "vite build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@homarr/db": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@nestjs/common": "^10.3.5",
"@nestjs/core": "^10.3.5",
"@nestjs/platform-express": "^10.3.5",
"nest-winston": "^1.9.4",
"reflect-metadata": "^0.2.1",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"sharp": "^0.33.3",
"vite": "^5.2.6",
"vite-plugin-node": "^3.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.5",
"@swc/core": "^1.4.8",
"@types/express": "^4.17.21",
"@types/node": "^20.12.2",
"@types/supertest": "^6.0.2",
"eslint": "^8.57.0",
"prettier": "^3.2.5",
"supertest": "^6.3.4",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.4.3"
},
"eslintConfig": {
"root": true,
"extends": [
"@homarr/eslint-config/base"
]
},
"prettier": "@homarr/prettier-config"
}

View File

@@ -1,39 +0,0 @@
/* eslint-disable @typescript-eslint/unbound-method */
import type { TestingModule } from "@nestjs/testing";
import { Test } from "@nestjs/testing";
import { beforeEach, describe, expect, it, vitest } from "vitest";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { DatabaseService } from "./db/database.service";
describe("AppController", () => {
let appController: AppController;
let appService: AppService;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService, DatabaseService],
}).compile();
appController = app.get<AppController>(AppController);
appService = app.get<AppService>(AppService);
});
describe("root", () => {
it('should return "Hello World!"', async () => {
// arrange
vitest
.spyOn(appService, "getHello")
.mockReturnValueOnce(Promise.resolve("ABC"));
// act
const app = await appController.getHello();
// assert
expect(app).toBe("ABC");
expect(appService.getHello).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -1,18 +0,0 @@
import { Controller, Get, Inject } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {
constructor(@Inject(AppService) private readonly appService: AppService) {}
@Get()
async getHello(): Promise<string> {
return await this.appService.getHello();
}
@Get("/random")
getRandom(): string {
return Math.random().toString();
}
}

View File

@@ -1,12 +0,0 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { DatabaseService } from "./db/database.service";
@Module({
imports: [],
controllers: [AppController],
providers: [DatabaseService, AppService],
})
export class AppModule {}

View File

@@ -1,15 +0,0 @@
import { Inject, Injectable } from "@nestjs/common";
import { DatabaseService } from "./db/database.service";
@Injectable()
export class AppService {
constructor(
@Inject(DatabaseService) private readonly databaseService: DatabaseService,
) {}
async getHello(): Promise<string> {
const users = await this.databaseService.get().query.users.findMany();
return JSON.stringify(users);
}
}

View File

@@ -1,10 +0,0 @@
import { Injectable } from "@nestjs/common";
import { db } from "@homarr/db";
@Injectable()
export class DatabaseService {
get() {
return db;
}
}

View File

@@ -1,27 +0,0 @@
import { NestFactory } from "@nestjs/core";
import { WinstonModule } from "nest-winston";
import { logger } from "@homarr/log";
import { AppModule } from "./app.module";
const winstonLoggerModule = WinstonModule.createLogger({
instance: logger,
});
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: winstonLoggerModule,
});
await app.listen(3100);
}
// @ts-expect-error this has no type yet
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (import.meta.env.PROD) {
void bootstrap();
}
export const viteNodeApp = NestFactory.create(AppModule, {
logger: winstonLoggerModule,
});

View File

@@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "dist", "**/*spec.ts"]
}

View File

@@ -1,19 +0,0 @@
{
"compilerOptions": {
"module": "CommonJS",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"strictNullChecks": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": ".",
"incremental": true,
"skipLibCheck": true
},
"include": ["src/**/*", "vite.config.ts"]
}

View File

@@ -1,34 +0,0 @@
import { defineConfig } from "vite";
import { VitePluginNode } from "vite-plugin-node";
export default defineConfig({
plugins: [
...VitePluginNode({
adapter: "nest",
appPath: "./src/main.ts",
tsCompiler: "swc",
}),
],
optimizeDeps: {
// Vite does not work well with optionnal dependencies,
// mark them as ignored for now
exclude: [
"@nestjs/microservices",
"@nestjs/websockets",
"cache-manager",
"class-transformer",
"class-validator",
"fastify-swagger",
],
esbuildOptions: {
tsconfigRaw: {
compilerOptions: {
experimentalDecorators: true,
},
},
},
},
server: {
port: 3100,
},
});

49
apps/tasks/package.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "@homarr/tasks",
"version": "0.1.0",
"private": true,
"exports": {
".": "./src/index.ts"
},
"main": "./src/main.ts",
"types": "./src/main.ts",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "pnpm with-env tsx ./src/main.ts",
"clean": "rm -rf .turbo node_modules",
"lint": "eslint .",
"format": "prettier --check . --ignore-path ../../.gitignore",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"dotenv": "^16.4.5",
"node-cron": "^3.0.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/node": "^20.12.2",
"dotenv-cli": "^7.4.1",
"eslint": "^8.57.0",
"prettier": "^3.2.5",
"tsx": "^4.7.1",
"typescript": "^5.4.3"
},
"eslintConfig": {
"root": true,
"extends": [
"@homarr/eslint-config/base"
]
},
"prettier": "@homarr/prettier-config"
}

3
apps/tasks/src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import { client } from "./queues";
export const queueClient = client;

9
apps/tasks/src/jobs.ts Normal file
View File

@@ -0,0 +1,9 @@
import { queuesJob } from "./jobs/queue";
import { createJobGroup } from "./lib/cron-job/group";
export const jobs = createJobGroup({
// Add your jobs here:
// This job is used to process queues.
queues: queuesJob,
});

View File

@@ -0,0 +1,8 @@
import { EVERY_MINUTE } from "../lib/cron-job/constants";
import { createCronJob } from "../lib/cron-job/creator";
import { queueWorker } from "../lib/queue/worker";
// This job processes queues, it runs every minute.
export const queuesJob = createCronJob(EVERY_MINUTE).withCallback(async () => {
await queueWorker();
});

View File

@@ -0,0 +1,5 @@
export const EVERY_5_SECONDS = "*/5 * * * * *";
export const EVERY_MINUTE = "* * * * *";
export const EVERY_5_MINUTES = "*/5 * * * *";
export const EVERY_10_MINUTES = "*/10 * * * *";
export const EVERY_HOUR = "0 * * * *";

View File

@@ -0,0 +1,17 @@
import cron from "node-cron";
import type { MaybePromise } from "@homarr/common/types";
export const createCronJob = (cronExpression: string) => {
return {
withCallback: (callback: () => MaybePromise<void>) => {
const task = cron.schedule(cronExpression, () => void callback(), {
scheduled: false,
});
return {
_expression: cronExpression,
_task: task,
};
},
};
};

View File

@@ -0,0 +1,48 @@
import { objectEntries } from "@homarr/common";
import type { createCronJob } from "./creator";
import { jobRegistry } from "./registry";
type Jobs = Record<
string,
ReturnType<ReturnType<typeof createCronJob>["withCallback"]>
>;
export const createJobGroup = <TJobs extends Jobs>(jobs: TJobs) => {
for (const [name, job] of objectEntries(jobs)) {
if (typeof name !== "string") continue;
jobRegistry.set(name, {
name,
expression: job._expression,
active: false,
task: job._task,
});
}
return {
start: (name: keyof TJobs) => {
const job = jobRegistry.get(name as string);
if (!job) return;
job.active = true;
job.task.start();
},
startAll: () => {
for (const job of jobRegistry.values()) {
job.active = true;
job.task.start();
}
},
stop: (name: keyof TJobs) => {
const job = jobRegistry.get(name as string);
if (!job) return;
job.active = false;
job.task.stop();
},
stopAll: () => {
for (const job of jobRegistry.values()) {
job.active = false;
job.task.stop();
}
},
};
};

View File

@@ -0,0 +1,9 @@
import type cron from "node-cron";
interface Job {
name: string;
expression: string;
active: boolean;
task: cron.ScheduledTask;
}
export const jobRegistry = new Map<string, Job>();

View File

@@ -0,0 +1,64 @@
import { objectEntries, objectKeys } from "@homarr/common";
import type { MaybePromise } from "@homarr/common/types";
import { queueChannel } from "@homarr/redis";
import type { z } from "@homarr/validation";
import type { createQueue } from "./creator";
interface Queue<TInput extends z.ZodType = z.ZodType> {
name: string;
callback: (input: z.infer<TInput>) => MaybePromise<void>;
inputValidator: TInput;
}
type Queues = Record<
string,
ReturnType<ReturnType<typeof createQueue>["withCallback"]>
>;
export const createQueueClient = <TQueues extends Queues>(queues: TQueues) => {
const queueRegistry = new Map<string, Queue>();
for (const [name, queue] of objectEntries(queues)) {
if (typeof name !== "string") continue;
queueRegistry.set(name, {
name,
callback: queue._callback,
inputValidator: queue._input,
});
}
return {
queueRegistry,
client: objectKeys(queues).reduce(
(acc, name) => {
acc[name] = async (
data: z.infer<TQueues[typeof name]["_input"]>,
options,
) => {
if (typeof name !== "string") return;
const queue = queueRegistry.get(name);
if (!queue) return;
await queueChannel.add({
name,
data,
executionDate:
typeof options === "object" && options.executionDate
? options.executionDate
: new Date(),
});
};
return acc;
},
{} as Record<
keyof TQueues,
(
data: z.infer<TQueues[keyof TQueues]["_input"]>,
props: {
executionDate?: Date;
} | void,
) => Promise<void>
>,
),
};
};

View File

@@ -0,0 +1,14 @@
import type { z } from "zod";
import type { MaybePromise } from "@homarr/common/types";
export const createQueue = <TInput extends z.ZodType>(input: TInput) => {
return {
withCallback: (callback: (data: z.infer<TInput>) => MaybePromise<void>) => {
return {
_input: input,
_callback: callback,
};
},
};
};

View File

@@ -0,0 +1,20 @@
import { queueChannel } from "@homarr/redis";
import { queueRegistry } from "~/queues";
/**
* This function reads all the queue executions that are due and processes them.
* Those executions are stored in the redis queue channel.
*/
export const queueWorker = async () => {
const now = new Date();
const executions = await queueChannel.filter((item) => {
return item.executionDate < now;
});
for (const execution of executions) {
const queue = queueRegistry.get(execution.name);
if (!queue) continue;
await queue.callback(execution.data);
await queueChannel.markAsDone(execution._id);
}
};

3
apps/tasks/src/main.ts Normal file
View File

@@ -0,0 +1,3 @@
import { jobs } from "./jobs";
jobs.startAll();

7
apps/tasks/src/queues.ts Normal file
View File

@@ -0,0 +1,7 @@
import { createQueueClient } from "./lib/queue/client";
import { testQueue } from "./queues/test";
export const { client, queueRegistry } = createQueueClient({
// Add your queues here
test: testQueue,
});

View File

@@ -0,0 +1,11 @@
import { z } from "@homarr/validation";
import { createQueue } from "~/lib/queue/creator";
export const testQueue = createQueue(
z.object({
id: z.string(),
}),
).withCallback(({ id }) => {
console.log(`Test queue with id ${id}`);
});

12
apps/tasks/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["src/*"]
},
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}