feat: snippet and function refactor (#692)

This commit is contained in:
2022-08-21 12:55:31 +08:00
committed by GitHub
parent 7571cac538
commit 095ccd711c
21 changed files with 281 additions and 83 deletions

View File

@@ -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',

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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'
}
}
}

View File

@@ -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')

View File

@@ -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
}

View File

@@ -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) {