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:
@@ -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>
|
|
||||||
<!--[](https://opencollective.com/nest#backer)
|
|
||||||
[](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).
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"collection": "@nestjs/schematics",
|
|
||||||
"sourceRoot": "src"
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 {}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
|
|
||||||
import { db } from "@homarr/db";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DatabaseService {
|
|
||||||
get() {
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"exclude": ["node_modules", "dist", "**/*spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
@@ -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
49
apps/tasks/package.json
Normal 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
3
apps/tasks/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { client } from "./queues";
|
||||||
|
|
||||||
|
export const queueClient = client;
|
||||||
9
apps/tasks/src/jobs.ts
Normal file
9
apps/tasks/src/jobs.ts
Normal 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,
|
||||||
|
});
|
||||||
8
apps/tasks/src/jobs/queue.ts
Normal file
8
apps/tasks/src/jobs/queue.ts
Normal 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();
|
||||||
|
});
|
||||||
5
apps/tasks/src/lib/cron-job/constants.ts
Normal file
5
apps/tasks/src/lib/cron-job/constants.ts
Normal 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 * * * *";
|
||||||
17
apps/tasks/src/lib/cron-job/creator.ts
Normal file
17
apps/tasks/src/lib/cron-job/creator.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
48
apps/tasks/src/lib/cron-job/group.ts
Normal file
48
apps/tasks/src/lib/cron-job/group.ts
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
9
apps/tasks/src/lib/cron-job/registry.ts
Normal file
9
apps/tasks/src/lib/cron-job/registry.ts
Normal 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>();
|
||||||
64
apps/tasks/src/lib/queue/client.ts
Normal file
64
apps/tasks/src/lib/queue/client.ts
Normal 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>
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
14
apps/tasks/src/lib/queue/creator.ts
Normal file
14
apps/tasks/src/lib/queue/creator.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
20
apps/tasks/src/lib/queue/worker.ts
Normal file
20
apps/tasks/src/lib/queue/worker.ts
Normal 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
3
apps/tasks/src/main.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { jobs } from "./jobs";
|
||||||
|
|
||||||
|
jobs.startAll();
|
||||||
7
apps/tasks/src/queues.ts
Normal file
7
apps/tasks/src/queues.ts
Normal 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,
|
||||||
|
});
|
||||||
11
apps/tasks/src/queues/test.ts
Normal file
11
apps/tasks/src/queues/test.ts
Normal 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
12
apps/tasks/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "@homarr/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["."],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"db:migration:generate": "pnpm -F db migration:generate",
|
"db:migration:generate": "pnpm -F db migration:generate",
|
||||||
"db:migration:run": "pnpm -F db migration:run",
|
"db:migration:run": "pnpm -F db migration:run",
|
||||||
"dev": "turbo dev --parallel",
|
"dev": "turbo dev --parallel",
|
||||||
|
"docker:dev": "docker compose -f ./development.docker-compose.yml up",
|
||||||
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
|
||||||
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
|
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
|
||||||
"lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache",
|
"lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/log": "workspace:^",
|
"@homarr/log": "workspace:^",
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
|
"@homarr/tasks": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0",
|
"@homarr/validation": "workspace:^0.1.0",
|
||||||
"@trpc/client": "next",
|
"@trpc/client": "next",
|
||||||
"@trpc/server": "next",
|
"@trpc/server": "next",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts"
|
".": "./index.ts",
|
||||||
|
"./types": "./src/types.ts"
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
1
packages/common/src/types.ts
Normal file
1
packages/common/src/types.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
@@ -22,7 +22,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"@homarr/log": "workspace:^"
|
"@homarr/log": "workspace:^",
|
||||||
|
"@homarr/db": "workspace:^",
|
||||||
|
"@homarr/common": "workspace:^"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
|
|||||||
@@ -1,40 +1,13 @@
|
|||||||
import { Redis } from "ioredis";
|
import { createQueueChannel, createSubPubChannel } from "./lib/channel";
|
||||||
import superjson from "superjson";
|
|
||||||
|
|
||||||
import { logger } from "@homarr/log";
|
export const exampleChannel = createSubPubChannel<{ message: string }>(
|
||||||
|
"example",
|
||||||
const subscriber = new Redis();
|
);
|
||||||
const publisher = new Redis();
|
export const queueChannel = createQueueChannel<{
|
||||||
const lastDataClient = new Redis();
|
name: string;
|
||||||
|
executionDate: Date;
|
||||||
const createChannel = <TData>(name: string) => {
|
data: unknown;
|
||||||
return {
|
}>("common-queue");
|
||||||
subscribe: (callback: (data: TData) => void) => {
|
|
||||||
void lastDataClient.get(`last-${name}`).then((data) => {
|
|
||||||
if (data) {
|
|
||||||
callback(superjson.parse(data));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
void subscriber.subscribe(name, (err) => {
|
|
||||||
if (!err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.error(
|
|
||||||
`Error with channel '${name}': ${err.name} (${err.message})`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
subscriber.on("message", (channel, message) => {
|
|
||||||
if (channel !== name) return;
|
|
||||||
|
|
||||||
callback(superjson.parse(message));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
publish: async (data: TData) => {
|
|
||||||
await lastDataClient.set(`last-${name}`, superjson.stringify(data));
|
|
||||||
await publisher.publish(name, superjson.stringify(data));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface LoggerMessage {
|
export interface LoggerMessage {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -42,6 +15,4 @@ export interface LoggerMessage {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loggingChannel = createChannel<LoggerMessage>("logging");
|
export const loggingChannel = createSubPubChannel<LoggerMessage>("logging");
|
||||||
|
|
||||||
export const exampleChannel = createChannel<{ message: string }>("example");
|
|
||||||
|
|||||||
116
packages/redis/src/lib/channel.ts
Normal file
116
packages/redis/src/lib/channel.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
import { createId } from "@homarr/db";
|
||||||
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { createRedisConnection } from "./connection";
|
||||||
|
|
||||||
|
const subscriber = createRedisConnection(); // Used for subscribing to channels - after subscribing it can only be used for subscribing
|
||||||
|
const publisher = createRedisConnection();
|
||||||
|
const lastDataClient = createRedisConnection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new pub/sub channel.
|
||||||
|
* @param name name of the channel
|
||||||
|
* @returns pub/sub channel object
|
||||||
|
*/
|
||||||
|
export const createSubPubChannel = <TData>(name: string) => {
|
||||||
|
const lastChannelName = `pubSub:last:${name}`;
|
||||||
|
const channelName = `pubSub:${name}`;
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Subscribes to the channel and calls the callback with the last data saved - when present.
|
||||||
|
* @param callback callback function to be called when new data is published
|
||||||
|
*/
|
||||||
|
subscribe: (callback: (data: TData) => void) => {
|
||||||
|
void lastDataClient.get(lastChannelName).then((data) => {
|
||||||
|
if (data) {
|
||||||
|
callback(superjson.parse(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
void subscriber.subscribe(channelName, (err) => {
|
||||||
|
if (!err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.error(
|
||||||
|
`Error with channel '${channelName}': ${err.name} (${err.message})`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
subscriber.on("message", (channel, message) => {
|
||||||
|
if (channel !== channelName) return;
|
||||||
|
|
||||||
|
callback(superjson.parse(message));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Publish data to the channel with last data saved.
|
||||||
|
* @param data data to be published
|
||||||
|
*/
|
||||||
|
publish: async (data: TData) => {
|
||||||
|
await lastDataClient.set(lastChannelName, superjson.stringify(data));
|
||||||
|
await publisher.publish(channelName, superjson.stringify(data));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueClient = createRedisConnection();
|
||||||
|
|
||||||
|
type WithId<TItem> = TItem & { _id: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a queue channel to store and manage queue executions.
|
||||||
|
* @param name name of the queue channel
|
||||||
|
* @returns queue channel object
|
||||||
|
*/
|
||||||
|
export const createQueueChannel = <TItem>(name: string) => {
|
||||||
|
const queueChannelName = `queue:${name}`;
|
||||||
|
const getData = async () => {
|
||||||
|
const data = await queueClient.get(queueChannelName);
|
||||||
|
return data ? superjson.parse<WithId<TItem>[]>(data) : [];
|
||||||
|
};
|
||||||
|
const setData = async (data: WithId<TItem>[]) => {
|
||||||
|
await queueClient.set(queueChannelName, superjson.stringify(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Add a new queue execution.
|
||||||
|
* @param data data to be stored in the queue execution to run it later
|
||||||
|
*/
|
||||||
|
add: async (data: TItem) => {
|
||||||
|
const items = await getData();
|
||||||
|
items.push({ _id: createId(), ...data });
|
||||||
|
await setData(items);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get all queue executions.
|
||||||
|
*/
|
||||||
|
all: getData,
|
||||||
|
/**
|
||||||
|
* Get a queue execution by its id.
|
||||||
|
* @param id id of the queue execution (stored under _id key)
|
||||||
|
* @returns queue execution or undefined if not found
|
||||||
|
*/
|
||||||
|
byId: async (id: string) => {
|
||||||
|
const items = await getData();
|
||||||
|
return items.find((item) => item._id === id);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the queue executions by a given filter function.
|
||||||
|
* @param filter callback function that returns true if the item should be included in the result
|
||||||
|
* @returns filtered queue executions
|
||||||
|
*/
|
||||||
|
filter: async (filter: (item: WithId<TItem>) => boolean) => {
|
||||||
|
const items = await getData();
|
||||||
|
return items.filter(filter);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Marks an queue execution as done, by deleting it.
|
||||||
|
* @param id id of the queue execution (stored under _id key)
|
||||||
|
*/
|
||||||
|
markAsDone: async (id: string) => {
|
||||||
|
const items = await getData();
|
||||||
|
await setData(items.filter((item) => item._id !== id));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
7
packages/redis/src/lib/connection.ts
Normal file
7
packages/redis/src/lib/connection.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Redis connection
|
||||||
|
* @returns redis client
|
||||||
|
*/
|
||||||
|
export const createRedisConnection = () => new Redis();
|
||||||
@@ -33,12 +33,13 @@
|
|||||||
},
|
},
|
||||||
"prettier": "@homarr/prettier-config",
|
"prettier": "@homarr/prettier-config",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@homarr/api": "workspace:^0.1.0",
|
||||||
"@homarr/common": "workspace:^0.1.0",
|
"@homarr/common": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/api": "workspace:^0.1.0",
|
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "workspace:^0.1.0"
|
"@homarr/validation": "workspace:^0.1.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import type { Loader } from "next/dynamic";
|
import type { Loader } from "next/dynamic";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { Loader as UiLoader } from "@homarr/ui";
|
import { Loader as UiLoader } from "@homarr/ui";
|
||||||
@@ -14,8 +14,8 @@ import * as weather from "./weather";
|
|||||||
export { reduceWidgetOptionsWithDefaultValues } from "./options";
|
export { reduceWidgetOptionsWithDefaultValues } from "./options";
|
||||||
|
|
||||||
export { WidgetEditModal } from "./modals/widget-edit-modal";
|
export { WidgetEditModal } from "./modals/widget-edit-modal";
|
||||||
export { GlobalItemServerDataRunner } from "./server/runner";
|
|
||||||
export { useServerDataFor } from "./server/provider";
|
export { useServerDataFor } from "./server/provider";
|
||||||
|
export { GlobalItemServerDataRunner } from "./server/runner";
|
||||||
|
|
||||||
export const widgetImports = {
|
export const widgetImports = {
|
||||||
clock,
|
clock,
|
||||||
|
|||||||
2014
pnpm-lock.yaml
generated
2014
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user