diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a88ae35ce..b8cd8a4a76 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -49,6 +49,7 @@ "ghaction", "gonanoid", "healthz", + "hokify", "hostpath", "Kube", "kubebuilder", diff --git a/runtimes/nodejs/src/handler/invoke-func.ts b/runtimes/nodejs/src/handler/invoke-func.ts index 168f5c211f..2756b2f8f7 100644 --- a/runtimes/nodejs/src/handler/invoke-func.ts +++ b/runtimes/nodejs/src/handler/invoke-func.ts @@ -11,6 +11,7 @@ import { logger } from '../support/logger' import { CloudFunction } from '../support/function-engine' import { IRequest } from '../support/types' import { handleDebugFunction } from './debug-func' +import { parseToken } from '../support/token' const DEFAULT_FUNCTION_NAME = '__default__' @@ -21,7 +22,12 @@ export async function handleInvokeFunction(req: IRequest, res: Response) { if (req.get('x-laf-debug-token')) { return await handleDebugFunction(req, res) } - + + let isTrigger = false + if (parseToken(req.get('x-laf-trigger-token'))) { + isTrigger = true + } + const requestId = req.requestId const func_name = req.params?.name @@ -38,7 +44,7 @@ export async function handleInvokeFunction(req: IRequest, res: Response) { const func = new CloudFunction(funcData) // reject while no HTTP enabled - if (!func.methods.includes(req.method.toUpperCase())) { + if (!func.methods.includes(req.method.toUpperCase()) && !isTrigger) { return res.status(405).send('Method Not Allowed') } @@ -49,7 +55,7 @@ export async function handleInvokeFunction(req: IRequest, res: Response) { files: req.files as any, body: req.body, headers: req.headers, - method: req.method, + method: isTrigger ? 'trigger' : req.method, auth: req['auth'], user: req.user, requestId, diff --git a/runtimes/nodejs/src/support/function-engine/types.ts b/runtimes/nodejs/src/support/function-engine/types.ts index c70c1b84a8..875c8c85fb 100644 --- a/runtimes/nodejs/src/support/function-engine/types.ts +++ b/runtimes/nodejs/src/support/function-engine/types.ts @@ -37,7 +37,9 @@ export interface FunctionContext { query?: any body?: any params?: any - // @Deprecated use user instead + /** + * @deprecated use user instead + */ auth?: any user?: any requestId: string diff --git a/server/package-lock.json b/server/package-lock.json index b878fbe376..69a020a822 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,7 @@ "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-sts": "^3.226.0", + "@hokify/agenda": "^6.3.0", "@kubernetes/client-node": "^0.17.1", "@nestjs/axios": "^1.0.0", "@nestjs/common": "^9.0.0", @@ -4275,6 +4276,30 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "node_modules/@hokify/agenda": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@hokify/agenda/-/agenda-6.3.0.tgz", + "integrity": "sha512-fWrKMDe/8QHJXLOdEsMogb6cb213Z82iNsnU7nFrSIMFifEXSkXNTyCZ99FV3KLf+Du1gS/M9/8uTC6FHyWRZQ==", + "dependencies": { + "cron-parser": "^4", + "date.js": "~0.3.3", + "debug": "~4", + "human-interval": "~2", + "luxon": "^3", + "mongodb": "^4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hokify/agenda/node_modules/luxon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz", + "integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==", + "engines": { + "node": ">=12" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -7754,6 +7779,25 @@ "luxon": "^1.23.x" } }, + "node_modules/cron-parser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.0.tgz", + "integrity": "sha512-BdAELR+MCT2ZWsIBhZKDuUqIUCBjHHulPJnm53OfdRLA4EWBjva3R+KM5NeidJuGsNXdEcZkjC7SCnkW5rAFSA==", + "dependencies": { + "luxon": "^3.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cron-parser/node_modules/luxon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz", + "integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==", + "engines": { + "node": ">=12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7859,11 +7903,31 @@ "lodash.unset": "4.5.2" } }, + "node_modules/date.js": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", + "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", + "dependencies": { + "debug": "~3.1.0" + } + }, + "node_modules/date.js/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/date.js/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -7879,8 +7943,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decache": { "version": "4.6.1", @@ -9763,6 +9826,14 @@ "npm": ">=1.3.7" } }, + "node_modules/human-interval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", + "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", + "dependencies": { + "numbered": "^1.1.0" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -12075,6 +12146,11 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/numbered": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", + "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==" + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -18669,6 +18745,26 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "@hokify/agenda": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@hokify/agenda/-/agenda-6.3.0.tgz", + "integrity": "sha512-fWrKMDe/8QHJXLOdEsMogb6cb213Z82iNsnU7nFrSIMFifEXSkXNTyCZ99FV3KLf+Du1gS/M9/8uTC6FHyWRZQ==", + "requires": { + "cron-parser": "^4", + "date.js": "~0.3.3", + "debug": "~4", + "human-interval": "~2", + "luxon": "^3", + "mongodb": "^4" + }, + "dependencies": { + "luxon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz", + "integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==" + } + } + }, "@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -21367,6 +21463,21 @@ "luxon": "^1.23.x" } }, + "cron-parser": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.7.0.tgz", + "integrity": "sha512-BdAELR+MCT2ZWsIBhZKDuUqIUCBjHHulPJnm53OfdRLA4EWBjva3R+KM5NeidJuGsNXdEcZkjC7SCnkW5rAFSA==", + "requires": { + "luxon": "^3.1.0" + }, + "dependencies": { + "luxon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.1.tgz", + "integrity": "sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw==" + } + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -21454,11 +21565,33 @@ "lodash.unset": "4.5.2" } }, + "date.js": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", + "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", + "requires": { + "debug": "~3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" }, @@ -21466,8 +21599,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -22901,6 +23033,14 @@ "sshpk": "^1.7.0" } }, + "human-interval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", + "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", + "requires": { + "numbered": "^1.1.0" + } + }, "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -24665,6 +24805,11 @@ "boolbase": "^1.0.0" } }, + "numbered": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", + "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==" + }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", diff --git a/server/package.json b/server/package.json index 6ce4adc972..6e5e37710d 100644 --- a/server/package.json +++ b/server/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@aws-sdk/client-sts": "^3.226.0", + "@hokify/agenda": "^6.3.0", "@kubernetes/client-node": "^0.17.1", "@nestjs/axios": "^1.0.0", "@nestjs/common": "^9.0.0", diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index ddb617929a..3a22a943f0 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -173,5 +173,19 @@ model CloudFunction { updatedAt DateTime @updatedAt createdBy String @db.ObjectId + cronTriggers CronTrigger[] + @@unique([appid, name]) } + +model CronTrigger { + id String @id @default(auto()) @map("_id") @db.ObjectId + appid String + desc String + cron String + target String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + cloudFunction CloudFunction @relation(fields: [appid, target], references: [appid, name]) +} diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 05a53c80b1..6932d87e30 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -16,6 +16,7 @@ import { PrismaService } from './prisma.service' import { StorageModule } from './storage/storage.module' import { LogModule } from './log/log.module' import { DependencyModule } from './dependency/dependency.module' +import { TriggerModule } from './trigger/trigger.module'; @Module({ imports: [ @@ -36,6 +37,7 @@ import { DependencyModule } from './dependency/dependency.module' StorageModule, LogModule, DependencyModule, + TriggerModule, ], controllers: [AppController], providers: [AppService, PrismaService], diff --git a/server/src/constants.ts b/server/src/constants.ts index 564788402a..f57d99bd68 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -3,6 +3,13 @@ dotenv.config({ path: '.env.local' }) dotenv.config() export class ServerConfig { + static get DATABASE_URL() { + if (!process.env.DATABASE_URL) { + throw new Error('DATABASE_URL is not defined') + } + return process.env.DATABASE_URL + } + static get JWT_SECRET() { if (!process.env.JWT_SECRET) { throw new Error('JWT_SECRET is not defined') diff --git a/server/src/trigger/agenda.service.ts b/server/src/trigger/agenda.service.ts new file mode 100644 index 0000000000..d411e4b160 --- /dev/null +++ b/server/src/trigger/agenda.service.ts @@ -0,0 +1,101 @@ +import { Injectable, Logger } from '@nestjs/common' +import { Agenda, Job } from '@hokify/agenda' +import { ServerConfig } from 'src/constants' +import { CronTrigger } from '@prisma/client' +import { GetApplicationNamespaceById } from 'src/utils/getter' +import * as assert from 'node:assert' +import { APPLICATION_SECRET_KEY } from 'src/constants' +import { JwtService } from '@nestjs/jwt' +import { PrismaService } from 'src/prisma.service' + +@Injectable() +export class AgendaService { + private readonly logger = new Logger(AgendaService.name) + private agenda: Agenda + public static JOB_NAME = 'TriggerCloudFunction' + + constructor( + private readonly jwtService: JwtService, + private readonly prisma: PrismaService, + ) { + this.agenda = new Agenda({ + db: { + address: ServerConfig.DATABASE_URL, + collection: 'CronJobs', + }, + ensureIndex: true, + }) + + this.agenda.define(AgendaService.JOB_NAME, async (job, done) => { + this.processor(job, done) + }) + + this.agenda.start().then(() => { + this.logger.log('Agenda started') + }) + } + + async createJob(trigger: CronTrigger) { + const { cron } = trigger + const job = await this.agenda.schedule( + cron, + AgendaService.JOB_NAME, + trigger, + ) + + return job + } + + async removeJob(trigger_id: string) { + const job = await this.agenda.jobs({ + name: AgendaService.JOB_NAME, + data: { + id: trigger_id, + }, + }) + if (job.length > 0) { + await job[0].remove() + } + } + + async processor(job: Job, done: (error?: Error) => void) { + const { appid, target } = job.attrs.data + this.logger.debug(`Triggering ${target} by cron job`) + + // generate trigger token + const token = this.getTriggerToken(appid) + const serviceName = appid + const namespace = GetApplicationNamespaceById(appid) + const appAddress = `${serviceName}.${namespace}:8000` + const url = `http://${appAddress}/${target}` + + await fetch(url, { + method: 'POST', + headers: { + 'x-laf-trigger-token': `${token}`, + }, + }) + done() + } + + async getTriggerToken(appid: string) { + const conf = await this.prisma.applicationConfiguration.findUnique({ + where: { appid }, + }) + + // get secret from envs + const secret = conf?.environments.find( + (env) => env.name === APPLICATION_SECRET_KEY, + ) + assert(secret?.value, 'application secret not found') + + // generate token + const exp = Math.floor(Date.now() / 1000) + 60 + + const token = this.jwtService.sign( + { appid, type: 'trigger', exp }, + { secret: secret.value }, + ) + return token + } +} diff --git a/server/src/trigger/dto/create-trigger.dto.ts b/server/src/trigger/dto/create-trigger.dto.ts new file mode 100644 index 0000000000..e808dc9b20 --- /dev/null +++ b/server/src/trigger/dto/create-trigger.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsNotEmpty, Length } from 'class-validator' + +export class CreateTriggerDto { + @ApiProperty() + @IsNotEmpty() + @Length(1, 64) + desc: string + + @ApiProperty() + @IsNotEmpty() + @Length(1, 64) + cron: string + + @ApiProperty() + @IsNotEmpty() + @Length(1, 255) + target: string +} diff --git a/server/src/trigger/dto/update-trigger.dto.ts b/server/src/trigger/dto/update-trigger.dto.ts new file mode 100644 index 0000000000..043b83e148 --- /dev/null +++ b/server/src/trigger/dto/update-trigger.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateTriggerDto } from './create-trigger.dto'; + +export class UpdateTriggerDto extends PartialType(CreateTriggerDto) {} diff --git a/server/src/trigger/trigger.controller.ts b/server/src/trigger/trigger.controller.ts new file mode 100644 index 0000000000..e504cc5158 --- /dev/null +++ b/server/src/trigger/trigger.controller.ts @@ -0,0 +1,74 @@ +import { + Controller, + Get, + Post, + Body, + Param, + Delete, + UseGuards, + Logger, +} from '@nestjs/common' +import { TriggerService } from './trigger.service' +import { CreateTriggerDto } from './dto/create-trigger.dto' +import { ResponseUtil } from 'src/utils/response' +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger' +import { JwtAuthGuard } from 'src/auth/jwt.auth.guard' +import { ApplicationAuthGuard } from 'src/auth/application.auth.guard' + +@ApiTags('Trigger') +@Controller('apps/:appid/triggers') +@ApiBearerAuth('Authorization') +export class TriggerController { + private readonly logger = new Logger(TriggerController.name) + constructor(private readonly triggerService: TriggerService) {} + + /** + * Create a cron trigger + * @param appid + * @param dto + * @returns + */ + @ApiOperation({ summary: 'Create a cron trigger' }) + @ApiResponse({ type: ResponseUtil }) + @UseGuards(JwtAuthGuard, ApplicationAuthGuard) + @Post() + async create(@Param('appid') appid: string, @Body() dto: CreateTriggerDto) { + const res = await this.triggerService.create(appid, dto) + return ResponseUtil.ok(res) + } + + /** + * Get trigger list of an application + * @param appid + * @returns + */ + @ApiOperation({ summary: 'Get trigger list of an application' }) + @ApiResponse({ type: ResponseUtil }) + @UseGuards(JwtAuthGuard, ApplicationAuthGuard) + @Get() + async findAll(@Param('appid') appid: string) { + const res = await this.triggerService.findAll(appid) + return ResponseUtil.ok(res) + } + + /** + * Delete a cron trigger + * @param appid + * @param id + * @returns + * @memberof TriggerController + */ + @ApiOperation({ summary: 'Remove a cron trigger' }) + @ApiResponse({ type: ResponseUtil }) + @UseGuards(JwtAuthGuard, ApplicationAuthGuard) + @Delete(':id') + async remove(@Param('appid') appid: string, @Param('id') id: string) { + const res = await this.triggerService.remove(appid, id) + return ResponseUtil.ok(res) + } +} diff --git a/server/src/trigger/trigger.module.ts b/server/src/trigger/trigger.module.ts new file mode 100644 index 0000000000..73ade36ce3 --- /dev/null +++ b/server/src/trigger/trigger.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common' +import { TriggerService } from './trigger.service' +import { TriggerController } from './trigger.controller' +import { AgendaService } from './agenda.service' +import { PrismaService } from 'src/prisma.service' +import { JwtService } from '@nestjs/jwt' +import { ApplicationService } from 'src/application/application.service' + +@Module({ + controllers: [TriggerController], + providers: [ + TriggerService, + AgendaService, + PrismaService, + JwtService, + ApplicationService, + ], +}) +export class TriggerModule {} diff --git a/server/src/trigger/trigger.service.ts b/server/src/trigger/trigger.service.ts new file mode 100644 index 0000000000..8eae847830 --- /dev/null +++ b/server/src/trigger/trigger.service.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from '@nestjs/common' +import { PrismaService } from 'src/prisma.service' +import { AgendaService } from './agenda.service' +import { CreateTriggerDto } from './dto/create-trigger.dto' + +@Injectable() +export class TriggerService { + private readonly logger = new Logger(TriggerService.name) + + constructor( + private readonly prisma: PrismaService, + private readonly agenda: AgendaService, + ) {} + + async create(appid: string, dto: CreateTriggerDto) { + const { desc, cron, target } = dto + const trigger = await this.prisma.cronTrigger.create({ + data: { + desc, + cron, + cloudFunction: { + connect: { + appid_name: { + appid, + name: target, + }, + }, + }, + }, + }) + + await this.agenda.createJob(trigger) + return trigger + } + + async findAll(appid: string) { + const res = await this.prisma.cronTrigger.findMany({ + where: { appid }, + }) + + return res + } + + async remove(appid: string, id: string) { + const res = await this.prisma.cronTrigger.deleteMany({ + where: { appid, id }, + }) + + await this.agenda.removeJob(id) + return res + } +}