feat: snippet and function refactor (#692)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Body, Post, Query, Request, Response } from '@nestjs/common'
|
||||
import { Body, Get, Post, Query, Request, Response } from '@nestjs/common'
|
||||
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
@@ -9,6 +9,7 @@ import { EventManagerService } from '~/processors/helper/helper.event.service'
|
||||
import { createMockedContextResponse } from '../serverless/mock-response.util'
|
||||
import { ServerlessService } from '../serverless/serverless.service'
|
||||
import { SnippetModel, SnippetType } from '../snippet/snippet.model'
|
||||
import { DebugService } from './debug.service'
|
||||
|
||||
@ApiName
|
||||
@ApiController('debug')
|
||||
@@ -16,8 +17,16 @@ export class DebugController {
|
||||
constructor(
|
||||
private readonly serverlessService: ServerlessService,
|
||||
private readonly eventManager: EventManagerService,
|
||||
|
||||
private readonly debugService: DebugService,
|
||||
) {}
|
||||
|
||||
@Get('/test')
|
||||
test() {
|
||||
this.debugService.test()
|
||||
return ''
|
||||
}
|
||||
|
||||
@Post('/events')
|
||||
async sendEvent(
|
||||
@Query('type') type: 'web' | 'admin' | 'all',
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { Inject, Injectable, Scope } from '@nestjs/common'
|
||||
import { REQUEST } from '@nestjs/core'
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class DebugService {
|
||||
constructor() {}
|
||||
constructor(@Inject(REQUEST) private req) {
|
||||
console.log('DebugService created')
|
||||
}
|
||||
|
||||
test() {
|
||||
console.log('this.req', this.req.method)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FastifyReply, FastifyRequest } from 'fastify'
|
||||
|
||||
import {
|
||||
All,
|
||||
CacheTTL,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
@@ -58,7 +59,7 @@ export class ServerlessController {
|
||||
return this.runServerlessFunction(param, isMaster, req, reply)
|
||||
}
|
||||
|
||||
@Get('/:reference/:name')
|
||||
@All('/:reference/:name')
|
||||
@HTTPDecorators.Bypass
|
||||
async runServerlessFunction(
|
||||
@Param() param: ServerlessReferenceDto,
|
||||
@@ -67,15 +68,25 @@ export class ServerlessController {
|
||||
@Request() req: FastifyRequest,
|
||||
@Response() reply: FastifyReply,
|
||||
) {
|
||||
const requestMethod = req.method.toUpperCase()
|
||||
const { name, reference } = param
|
||||
const snippet = await this.serverlessService.model.findOne({
|
||||
name,
|
||||
reference,
|
||||
type: SnippetType.Function,
|
||||
})
|
||||
const snippet = await this.serverlessService.model
|
||||
.findOne({
|
||||
name,
|
||||
reference,
|
||||
type: SnippetType.Function,
|
||||
method: requestMethod,
|
||||
})
|
||||
.lean()
|
||||
|
||||
const notExistMessage = 'serverless function is not exist or not enabled'
|
||||
|
||||
if (!snippet) {
|
||||
throw new NotFoundException('serverless function is not exist')
|
||||
throw new NotFoundException(notExistMessage)
|
||||
}
|
||||
|
||||
if (snippet.method !== requestMethod || !snippet.enable) {
|
||||
throw new NotFoundException(notExistMessage)
|
||||
}
|
||||
|
||||
if (snippet.private && !isMaster) {
|
||||
|
||||
@@ -375,7 +375,7 @@ export class ServerlessService {
|
||||
private lruCache = new LRUCache({
|
||||
max: 100,
|
||||
ttl: 10 * 1000,
|
||||
maxSize: 5000,
|
||||
maxSize: 50000,
|
||||
sizeCalculation: (value: string, key: string) => {
|
||||
return value.length + key.length
|
||||
},
|
||||
@@ -563,8 +563,12 @@ export class ServerlessService {
|
||||
const { body } = ast.program as t.Program
|
||||
|
||||
const hasEntryFunction = body.some(
|
||||
(node) => t.isFunction(node) && node.id && node.id.name === 'handler',
|
||||
(node: t.Declaration) =>
|
||||
(node.type == 'ExportDefaultDeclaration' &&
|
||||
isHandlerFunction(node.declaration)) ||
|
||||
isHandlerFunction(node),
|
||||
)
|
||||
|
||||
return hasEntryFunction
|
||||
} catch (e) {
|
||||
if (isDev) {
|
||||
@@ -572,5 +576,17 @@ export class ServerlessService {
|
||||
}
|
||||
return e.message?.split('\n').at(0)
|
||||
}
|
||||
|
||||
function isHandlerFunction(
|
||||
node:
|
||||
| t.Declaration
|
||||
| t.FunctionDeclaration
|
||||
| t.ClassDeclaration
|
||||
| t.TSDeclareFunction
|
||||
| t.Expression,
|
||||
): boolean {
|
||||
// @ts-expect-error
|
||||
return t.isFunction(node) && node?.id?.name === 'handler'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ import {
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common'
|
||||
|
||||
import { ApiController } from '~/common/decorator/api-controller.decorator'
|
||||
@@ -15,7 +17,6 @@ import { BanInDemo } from '~/common/decorator/demo.decorator'
|
||||
import { HTTPDecorators } from '~/common/decorator/http.decorator'
|
||||
import { ApiName } from '~/common/decorator/openapi.decorator'
|
||||
import { IsMaster } from '~/common/decorator/role.decorator'
|
||||
import { CacheService } from '~/processors/redis/cache.service'
|
||||
import { MongoIdDto } from '~/shared/dto/id.dto'
|
||||
import { PagerDto } from '~/shared/dto/pager.dto'
|
||||
import { transformDataToPaginate } from '~/transformers/paginate.transformer'
|
||||
@@ -27,10 +28,7 @@ import { SnippetService } from './snippet.service'
|
||||
@ApiName
|
||||
@ApiController('snippets')
|
||||
export class SnippetController {
|
||||
constructor(
|
||||
private readonly snippetService: SnippetService,
|
||||
private readonly redisService: CacheService,
|
||||
) {}
|
||||
constructor(private readonly snippetService: SnippetService) {}
|
||||
|
||||
@Get('/')
|
||||
@Auth()
|
||||
@@ -50,9 +48,9 @@ export class SnippetController {
|
||||
)
|
||||
}
|
||||
|
||||
@Post('/more')
|
||||
@Post('/import')
|
||||
@Auth()
|
||||
async createMore(@Body() body: SnippetMoreDto) {
|
||||
async importSnippets(@Body() body: SnippetMoreDto) {
|
||||
const { snippets } = body
|
||||
const tasks = snippets.map((snippet) => this.create(snippet))
|
||||
|
||||
@@ -78,6 +76,51 @@ export class SnippetController {
|
||||
return snippet
|
||||
}
|
||||
|
||||
@Get('/group')
|
||||
@Auth()
|
||||
@HTTPDecorators.Paginator
|
||||
async getGroup(@Query() query: PagerDto) {
|
||||
const { page, size = 30 } = query
|
||||
return this.snippetService.model.aggregatePaginate(
|
||||
this.snippetService.model.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
reference: '$reference',
|
||||
},
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
'_id.reference': 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
reference: '$_id.reference',
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
]),
|
||||
{
|
||||
page,
|
||||
limit: size,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Get('/group/:reference')
|
||||
@Auth()
|
||||
async getGroupByReference(@Param('reference') reference: string) {
|
||||
if (typeof reference !== 'string') {
|
||||
throw new UnprocessableEntityException('reference should be string')
|
||||
}
|
||||
|
||||
return this.snippetService.model.find({ reference }).lean()
|
||||
}
|
||||
|
||||
@Post('/aggregate')
|
||||
@Auth()
|
||||
async aggregate(@Body() body: any) {
|
||||
@@ -123,16 +166,19 @@ export class SnippetController {
|
||||
}
|
||||
|
||||
const snippet = await this.snippetService.getSnippetByName(name, reference)
|
||||
|
||||
if (snippet.type === SnippetType.Function) {
|
||||
throw new NotFoundException()
|
||||
}
|
||||
|
||||
if (snippet.private && !isMaster) {
|
||||
throw new ForbiddenException('snippet is private')
|
||||
}
|
||||
|
||||
if (snippet.type !== SnippetType.Function) {
|
||||
return this.snippetService.attachSnippet(snippet).then((res) => {
|
||||
this.snippetService.cacheSnippet(res, res.data)
|
||||
return res.data
|
||||
})
|
||||
}
|
||||
return this.snippetService.attachSnippet(snippet).then((res) => {
|
||||
this.snippetService.cacheSnippet(res, res.data)
|
||||
return res.data
|
||||
})
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
|
||||
@@ -8,8 +8,9 @@ import {
|
||||
Matches,
|
||||
MaxLength,
|
||||
} from 'class-validator'
|
||||
import aggregatePaginate from 'mongoose-aggregate-paginate-v2'
|
||||
|
||||
import { index, modelOptions, prop } from '@typegoose/typegoose'
|
||||
import { index, modelOptions, plugin, prop } from '@typegoose/typegoose'
|
||||
|
||||
import { BaseModel } from '~/shared/model/base.model'
|
||||
|
||||
@@ -32,6 +33,7 @@ export enum SnippetType {
|
||||
},
|
||||
},
|
||||
})
|
||||
@plugin(aggregatePaginate)
|
||||
@index({ name: 1, reference: 1 })
|
||||
@index({ type: 1 })
|
||||
export class SnippetModel extends BaseModel {
|
||||
@@ -81,4 +83,15 @@ export class SnippetModel extends BaseModel {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
schema?: string
|
||||
|
||||
// for function
|
||||
@prop()
|
||||
@IsEnum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
|
||||
@IsOptional()
|
||||
method?: string
|
||||
|
||||
@prop()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
enable?: boolean
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { load } from 'js-yaml'
|
||||
import JSON5 from 'json5'
|
||||
import { AggregatePaginateModel, Document } from 'mongoose'
|
||||
|
||||
import {
|
||||
BadRequestException,
|
||||
@@ -21,7 +22,8 @@ import { SnippetModel, SnippetType } from './snippet.model'
|
||||
export class SnippetService {
|
||||
constructor(
|
||||
@InjectModel(SnippetModel)
|
||||
private readonly snippetModel: MongooseModel<SnippetModel>,
|
||||
private readonly snippetModel: MongooseModel<SnippetModel> &
|
||||
AggregatePaginateModel<SnippetModel & Document>,
|
||||
@Inject(forwardRef(() => ServerlessService))
|
||||
private readonly serverlessService: ServerlessService,
|
||||
private readonly cacheService: CacheService,
|
||||
@@ -32,26 +34,42 @@ export class SnippetService {
|
||||
}
|
||||
|
||||
async create(model: SnippetModel) {
|
||||
if (model.type === SnippetType.Function) {
|
||||
model.method ??= 'GET'
|
||||
model.enable ??= true
|
||||
}
|
||||
const isExist = await this.model.countDocuments({
|
||||
name: model.name,
|
||||
reference: model.reference || 'root',
|
||||
method: model.method,
|
||||
})
|
||||
|
||||
if (isExist) {
|
||||
throw new BadRequestException('snippet is exist')
|
||||
}
|
||||
// 验证正确类型
|
||||
await this.validateType(model)
|
||||
await this.validateTypeAndCleanup(model)
|
||||
return await this.model.create({ ...model, created: new Date() })
|
||||
}
|
||||
|
||||
async update(id: string, model: SnippetModel) {
|
||||
await this.validateType(model)
|
||||
await this.validateTypeAndCleanup(model)
|
||||
delete model.created
|
||||
const old = await this.model.findById(id).lean()
|
||||
|
||||
if (!old) {
|
||||
throw new NotFoundException()
|
||||
}
|
||||
|
||||
if (
|
||||
old.type === SnippetType.Function &&
|
||||
model.type !== SnippetType.Function
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'`type` is not allowed to change if this snippet set to Function type.',
|
||||
)
|
||||
}
|
||||
|
||||
await this.deleteCachedSnippet(old.reference, old.name)
|
||||
return await this.model.findByIdAndUpdate(
|
||||
id,
|
||||
@@ -68,7 +86,7 @@ export class SnippetService {
|
||||
await this.deleteCachedSnippet(doc.reference, doc.name)
|
||||
}
|
||||
|
||||
private async validateType(model: SnippetModel) {
|
||||
private async validateTypeAndCleanup(model: SnippetModel) {
|
||||
switch (model.type) {
|
||||
case SnippetType.JSON: {
|
||||
try {
|
||||
@@ -113,6 +131,14 @@ export class SnippetService {
|
||||
break
|
||||
}
|
||||
}
|
||||
// TODO refactor
|
||||
// cleanup
|
||||
if (model.type !== SnippetType.Function) {
|
||||
const deleteKeys: (keyof SnippetModel)[] = ['enable', 'method']
|
||||
deleteKeys.forEach((key) => {
|
||||
Reflect.deleteProperty(model, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async getSnippetById(id: string) {
|
||||
|
||||
Reference in New Issue
Block a user